// KeysList responds with the list of keys assigned to the spiecified email func KeysList(w http.ResponseWriter, r *http.Request) { // Get the username from the GET query user := r.URL.Query().Get("user") if user == "" { utils.JSONResponse(w, 409, &KeysListResponse{ Success: false, Message: "Invalid username", }) return } user = utils.RemoveDots(utils.NormalizeUsername(user)) address, err := env.Addresses.GetAddress(user) if err != nil { utils.JSONResponse(w, 409, &KeysListResponse{ Success: false, Message: "Invalid address", }) return } // Find all keys owner by user keys, err := env.Keys.FindByOwner(address.Owner) if err != nil { utils.JSONResponse(w, 500, &KeysListResponse{ Success: false, Message: "Internal server error (KE//LI/01)", }) return } // Respond with list of keys utils.JSONResponse(w, 200, &KeysListResponse{ Success: true, Keys: &keys, }) }
func PrepareHandler(config *shared.Flags) func(peer smtpd.Peer, env smtpd.Envelope) error { cfg = config // Initialize a new logger log := logrus.New() if config.LogFormatterType == "text" { log.Formatter = &logrus.TextFormatter{ ForceColors: config.ForceColors, } } else if config.LogFormatterType == "json" { log.Formatter = &logrus.JSONFormatter{} } log.Level = logrus.DebugLevel // Initialize the database connection var err error session, err = gorethink.Connect(gorethink.ConnectOpts{ Address: config.RethinkAddress, AuthKey: config.RethinkKey, MaxIdle: 10, Timeout: time.Second * 10, }) if err != nil { log.WithFields(logrus.Fields{ "error": err.Error(), }).Fatal("Unable to connect to RethinkDB") } // Connect to NSQ producer, err := nsq.NewProducer(config.NSQDAddress, nsq.NewConfig()) if err != nil { log.WithFields(logrus.Fields{ "error": err.Error(), }).Fatal("Unable to connect to NSQd") } // Create a new spamd client spam := spamc.New(config.SpamdAddress, 10) // Last message sent by PrepareHandler log.WithFields(logrus.Fields{ "addr": config.BindAddress, }).Info("Listening for incoming traffic") return func(peer smtpd.Peer, e smtpd.Envelope) error { log.Debug("Started parsing") // Check recipients for Lavaboom users recipients := []interface{}{} for _, recipient := range e.Recipients { log.Printf("EMAIL TO %s", recipient) // Split the email address into username and domain parts := strings.Split(recipient, "@") if len(parts) != 2 { return describeError(fmt.Errorf("Invalid recipient email address")) } // Check if we support that domain if _, ok := domains[parts[1]]; ok { recipients = append(recipients, utils.RemoveDots( utils.NormalizeUsername(parts[0]), ), ) } } log.Debug("Parsed recipients") // If we didn't find a recipient, return an error if len(recipients) == 0 { return describeError(fmt.Errorf("Not supported email domain")) } // Fetch the mapping cursor, err := gorethink.Db(config.RethinkDatabase).Table("addresses").GetAll(recipients...).Run(session) if err != nil { return describeError(err) } defer cursor.Close() var addresses []*models.Address if err := cursor.All(&addresses); err != nil { return describeError(err) } // Transform the mapping into accounts accountIDs := []interface{}{} for _, address := range addresses { accountIDs = append(accountIDs, address.Owner) } // Fetch accounts cursor, err = gorethink.Db(config.RethinkDatabase).Table("accounts").GetAll(accountIDs...).Run(session) if err != nil { return describeError(err) } defer cursor.Close() var accounts []*models.Account if err := cursor.All(&accounts); err != nil { return describeError(err) } // Compare request and result lengths if len(accounts) != len(recipients) { return describeError(fmt.Errorf("One of the email addresses wasn't found")) } log.Debug("Recipients found") // Prepare a variable for the combined keyring of recipients toKeyring := []*openpgp.Entity{} // Fetch users' public keys for _, account := range accounts { account.Key, err = getAccountPublicKey(account) if err != nil { return describeError(err) } toKeyring = append(toKeyring, account.Key) } log.Debug("Fetched keys") // Check in the antispam isSpam := false spamReply, err := spam.Report(string(e.Data)) if err == nil { log.Print(spamReply.Code) log.Print(spamReply.Message) log.Print(spamReply.Vars) } if spamReply.Code == spamc.EX_OK { log.Print("Proper code") if spam, ok := spamReply.Vars["isSpam"]; ok && spam.(bool) { log.Print("It's spam.") isSpam = true } } // Parse the email email, err := ParseEmail(bytes.NewReader(e.Data)) if err != nil { return describeError(err) } // Determine email's kind contentType := email.Headers.Get("Content-Type") kind := "raw" if strings.HasPrefix(contentType, "multipart/encrypted") { // multipart/encrypted is dedicated for PGP/MIME and S/MIME kind = "pgpmime" } else if strings.HasPrefix(contentType, "multipart/mixed") && len(email.Children) >= 2 { // Has manifest? It is an email with a PGP manifest. If not, it's unencrypted. for _, child := range email.Children { if strings.HasPrefix(child.Headers.Get("Content-Type"), "application/x-pgp-manifest") { kind = "manifest" break } } } // Copy kind to a second variable for later parsing initialKind := kind // Debug the kind log.Debugf("Email is %s", kind) // Declare variables used later for data insertion var ( subject string manifest string body string fileIDs = map[string][]string{} files = []*models.File{} ) // Transform raw emails into encrypted with manifests if kind == "raw" { // Prepare variables for manifest generation parts := []*man.Part{} // Parsing vars var ( bodyType string bodyText string ) // Flatten the email var parseBody func(msg *Message) error parseBody = func(msg *Message) error { contentType := msg.Headers.Get("Content-Type") if strings.HasPrefix(contentType, "multipart/alternative") { preferredType := "" preferredIndex := -1 // Find the best body for index, child := range msg.Children { contentType := child.Headers.Get("Content-Type") if strings.HasPrefix(contentType, "application/pgp-encrypted") { preferredType = "pgp" preferredIndex = index break } if strings.HasPrefix(contentType, "text/html") { preferredType = "html" preferredIndex = index } if strings.HasPrefix(contentType, "text/plain") { if preferredType != "html" { preferredType = "plain" preferredIndex = index } } } if preferredIndex == -1 && len(msg.Children) > 0 { preferredIndex = 0 } else if preferredIndex == -1 { return nil // crappy email } // Parse its media type to remove non-required stuff match := msg.Children[preferredIndex] mediaType, _, err := mime.ParseMediaType(match.Headers.Get("Content-Type")) if err != nil { return describeError(err) } // Push contents into the parser's scope bodyType = mediaType bodyText = string(match.Body) /* change of plans - discard them. // Transform rest of the types into attachments nodeID := uniuri.New() for _, child := range msg.Children { child.Headers["disposition"] = "attachment; filename=\"alternative." + nodeID + "." + mime. +"\"" }*/ } else if strings.HasPrefix(contentType, "multipart/") { // Tread every other multipart as multipart/mixed, as we parse multipart/encrypted later for _, child := range msg.Children { if err := parseBody(child); err != nil { return describeError(err) } } } else { // Parse the content type mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { return describeError(err) } // Not multipart, parse the disposition disposition, dparams, err := mime.ParseMediaType(msg.Headers.Get("Content-Disposition")) if err == nil && disposition == "attachment" { // We're dealing with an attachment id := uniuri.NewLen(uniuri.UUIDLen) // Encrypt the body encryptedBody, err := shared.EncryptAndArmor(msg.Body, toKeyring) if err != nil { return describeError(err) } // Hash the body rawHash := sha256.Sum256(msg.Body) hash := hex.EncodeToString(rawHash[:]) // Push the attachment into parser's scope parts = append(parts, &man.Part{ Hash: hash, ID: id, ContentType: mediaType, Filename: dparams["filename"], Size: len(msg.Body), }) for _, account := range accounts { fid := uniuri.NewLen(uniuri.UUIDLen) files = append(files, &models.File{ Resource: models.Resource{ ID: fid, DateCreated: time.Now(), DateModified: time.Now(), Name: id + ".pgp", Owner: account.ID, }, Encrypted: models.Encrypted{ Encoding: "application/pgp-encrypted", Data: string(encryptedBody), }, }) if _, ok := fileIDs[account.ID]; !ok { fileIDs[account.ID] = []string{} } fileIDs[account.ID] = append(fileIDs[account.ID], fid) } } else { // Header is either corrupted or we're dealing with inline if bodyType == "" && mediaType == "text/plain" || mediaType == "text/html" { bodyType = mediaType bodyText = string(msg.Body) } else if bodyType == "" { bodyType = "text/html" if strings.Index(mediaType, "image/") == 0 { bodyText = `<img src="data:` + mediaType + `;base64,` + base64.StdEncoding.EncodeToString(msg.Body) + `"><br>` } else { bodyText = "<pre>" + string(msg.Body) + "</pre>" } } else if mediaType == "text/plain" { if bodyType == "text/plain" { bodyText += "\n\n" + string(msg.Body) } else { bodyText += "\n\n<pre>" + string(msg.Body) + "</pre>" } } else if mediaType == "text/html" { if bodyType == "text/plain" { bodyType = "text/html" bodyText = "<pre>" + bodyText + "</pre>\n\n" + string(msg.Body) } else { bodyText += "\n\n" + string(msg.Body) } } else { if bodyType != "text/html" { bodyType = "text/html" bodyText = "<pre>" + bodyText + "</pre>" } // Put images as HTML tags if strings.Index(mediaType, "image/") == 0 { bodyText = "\n\n<img src=\"data:" + mediaType + ";base64," + base64.StdEncoding.EncodeToString(msg.Body) + "\"><br>" } else { bodyText = "\n\n<pre>" + string(msg.Body) + "</pre>" } } } } return nil } // Parse the email parseBody(email) // Trim the body text bodyText = strings.TrimSpace(bodyText) // Hash the body bodyHash := sha256.Sum256([]byte(bodyText)) // Append body to the parts parts = append(parts, &man.Part{ Hash: hex.EncodeToString(bodyHash[:]), ID: "body", ContentType: bodyType, Size: len(bodyText), }) // Debug info log.Debug("Finished parsing the email") // Push files into RethinkDB for _, file := range files { if err := gorethink.Db(config.RethinkDatabase).Table("files").Insert(file).Exec(session); err != nil { return describeError(err) } } // Generate the from, to and cc addresses from, err := email.Headers.AddressList("from") if err != nil { from = []*mail.Address{} } to, err := email.Headers.AddressList("to") if err != nil { to = []*mail.Address{} } cc, err := email.Headers.AddressList("cc") if err != nil { cc = []*mail.Address{} } // Generate the manifest emailID := uniuri.NewLen(uniuri.UUIDLen) subject = "Encrypted message (" + emailID + ")" s2 := email.Headers.Get("subject") if len(s2) > 1 && s2[0] == '=' && s2[1] == '?' { s2, _, err = quotedprintable.DecodeHeader(s2) if err != nil { return describeError(err) } } var fm *mail.Address if len(from) > 0 { fm = from[0] } else { fm = &mail.Address{ Name: "no from header", Address: "invalid", } } rawManifest := &man.Manifest{ Version: semver.Version{ Major: 1, }, From: fm, To: to, CC: cc, Subject: s2, Parts: parts, } // Encrypt the manifest and the body encryptedBody, err := shared.EncryptAndArmor([]byte(bodyText), toKeyring) if err != nil { return describeError(err) } strManifest, err := man.Write(rawManifest) if err != nil { return describeError(err) } encryptedManifest, err := shared.EncryptAndArmor(strManifest, toKeyring) if err != nil { return describeError(err) } body = string(encryptedBody) manifest = string(encryptedManifest) kind = "manifest" _ = subject } else if kind == "manifest" { // Variables used for attachment search manifestIndex := -1 bodyIndex := -1 // Find indexes of the manifest and the body for index, child := range email.Children { contentType := child.Headers.Get("Content-Type") if strings.Index(contentType, "application/x-pgp-manifest") == 0 { manifestIndex = index } else if strings.Index(contentType, "multipart/alternative") == 0 { bodyIndex = index } if manifestIndex != -1 && bodyIndex != -1 { break } } // Check that we found both parts if manifestIndex == -1 || bodyIndex == -1 { return describeError(fmt.Errorf("Invalid PGP/Manifest email")) } // Search for the body child index bodyChildIndex := -1 for index, child := range email.Children[bodyIndex].Children { contentType := child.Headers.Get("Content-Type") if strings.Index(contentType, "application/pgp-encrypted") == 0 { bodyChildIndex = index break } } // Check that we found it if bodyChildIndex == -1 { return describeError(fmt.Errorf("Invalid PGP/Manifest email body")) } // Find the manifest and the body manifest = string(email.Children[manifestIndex].Body) body = string(email.Children[bodyIndex].Children[bodyChildIndex].Body) subject = "Encrypted email" // Gather attachments and insert them into db for index, child := range email.Children { if index == bodyIndex || index == manifestIndex { continue } _, cdparams, err := mime.ParseMediaType(child.Headers.Get("Content-Disposition")) if err != nil { return describeError(err) } for _, account := range accounts { fid := uniuri.NewLen(uniuri.UUIDLen) if err := gorethink.Db(config.RethinkDatabase).Table("files").Insert(&models.File{ Resource: models.Resource{ ID: fid, DateCreated: time.Now(), DateModified: time.Now(), Name: cdparams["filename"], Owner: account.ID, }, Encrypted: models.Encrypted{ Encoding: "application/pgp-encrypted", Data: string(child.Body), }, }).Exec(session); err != nil { return describeError(err) } if _, ok := fileIDs[account.ID]; !ok { fileIDs[account.ID] = []string{} } fileIDs[account.ID] = append(fileIDs[account.ID], fid) } } } else if kind == "pgpmime" { for _, child := range email.Children { if strings.Index(child.Headers.Get("Content-Type"), "application/pgp-encrypted") != -1 { body = string(child.Body) subject = child.Headers.Get("Subject") break } } } if len(subject) > 1 && subject[0] == '=' && subject[1] == '?' { subject, _, err = quotedprintable.DecodeHeader(subject) if err != nil { return describeError(err) } } // Save the email for each recipient for _, account := range accounts { // Get 3 user's labels cursor, err := gorethink.Db(config.RethinkDatabase).Table("labels").GetAllByIndex("nameOwnerBuiltin", []interface{}{ "Inbox", account.ID, true, }, []interface{}{ "Spam", account.ID, true, }, []interface{}{ "Trash", account.ID, true, }, []interface{}{}).Run(session) if err != nil { return describeError(err) } defer cursor.Close() var labels []*models.Label if err := cursor.All(&labels); err != nil { return describeError(err) } var ( inbox = labels[0] spam = labels[1] trash = labels[2] ) // Get the subject's hash subjectHash := email.Headers.Get("Subject-Hash") if subjectHash == "" { subject := email.Headers.Get("Subject") if subject == "" { subject = "<no subject>" } if len(subject) > 1 && subject[0] == '=' && subject[1] == '?' { subject, _, err = quotedprintable.DecodeHeader(subject) if err != nil { return describeError(err) } } subject = shared.StripPrefixes(strings.TrimSpace(subject)) hash := sha256.Sum256([]byte(subject)) subjectHash = hex.EncodeToString(hash[:]) } // Generate the email ID eid := uniuri.NewLen(uniuri.UUIDLen) // Prepare from, to and cc from := email.Headers.Get("from") if f1, err := email.Headers.AddressList("from"); err == nil && len(f1) > 0 { from = strings.TrimSpace(f1[0].Name + " <" + f1[0].Address + ">") } to := strings.Split(email.Headers.Get("to"), ", ") cc := strings.Split(email.Headers.Get("cc"), ", ") for i, v := range to { to[i] = strings.TrimSpace(v) } for i, v := range cc { cc[i] = strings.TrimSpace(v) } if len(cc) == 1 && cc[0] == "" { cc = nil } // Transform headers into map[string]string fh := map[string]string{} for key, values := range email.Headers { fh[key] = strings.Join(values, ", ") } // Find the thread var thread *models.Thread // First check if either in-reply-to or references headers are set irt := "" if x := email.Headers.Get("In-Reply-To"); x != "" { irt = x } else if x := email.Headers.Get("References"); x != "" { irt = x } if irt != "" { // Per http://www.jwz.org/doc/threading.html: // You can safely assume that the first string between <> in In-Reply-To // is the message ID. x1i := strings.Index(irt, "<") if x1i != -1 { x2i := strings.Index(irt[x1i+1:], ">") if x2i != -1 { irt = irt[x1i+1 : x1i+x2i+1] } } // Look up the parent cursor, err := gorethink.Db(config.RethinkDatabase).Table("emails").GetAllByIndex("messageIDOwner", []interface{}{ irt, account.ID, }).Run(session) if err != nil { return describeError(err) } defer cursor.Close() var emails []*models.Email if err := cursor.All(&emails); err != nil { return describeError(err) } // Found one = that one is correct if len(emails) == 1 { cursor, err := gorethink.Db(config.RethinkDatabase).Table("threads").Get(emails[0].Thread).Run(session) if err != nil { return describeError(err) } defer cursor.Close() if err := cursor.One(&thread); err != nil { return describeError(err) } } } if thread == nil { // Match by subject cursor, err := gorethink.Db(config.RethinkDatabase).Table("threads").GetAllByIndex("subjectOwner", []interface{}{ subjectHash, account.ID, }).Filter(func(row gorethink.Term) gorethink.Term { return row.Field("members").Map(func(member gorethink.Term) gorethink.Term { return member.Match(gorethink.Expr(from)).CoerceTo("string").Ne("null") }).Contains(gorethink.Expr(true)).And( gorethink.Not( row.Field("labels").Contains(spam.ID).Or( row.Field("labels").Contains(trash.ID), ), ), ) }).Run(session) if err != nil { return describeError(err) } defer cursor.Close() var threads []*models.Thread if err := cursor.All(&threads); err != nil { return describeError(err) } if len(threads) > 0 { thread = threads[0] } } if thread == nil { secure := "all" if initialKind == "raw" { secure = "none" } labels := []string{inbox.ID} if isSpam { labels = append(labels, spam.ID) } thread = &models.Thread{ Resource: models.Resource{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Name: "Encrypted thread", Owner: account.ID, }, Emails: []string{eid}, Labels: labels, Members: append(append(to, cc...), from), IsRead: false, SubjectHash: subjectHash, Secure: secure, } if err := gorethink.Db(config.RethinkDatabase).Table("threads").Insert(thread).Exec(session); err != nil { return describeError(err) } } else { var desiredID string if isSpam { desiredID = spam.ID } else { desiredID = inbox.ID } foundLabel := false for _, label := range thread.Labels { if label == desiredID { foundLabel = true break } } if !foundLabel { thread.Labels = append(thread.Labels, desiredID) } thread.Emails = append(thread.Emails, eid) update := map[string]interface{}{ "date_modified": gorethink.Now(), "is_read": false, "labels": thread.Labels, "emails": thread.Emails, } // update thread.secure depending on email's kind if (initialKind == "raw" && thread.Secure == "all") || (initialKind == "manifest" && thread.Secure == "none") || (initialKind == "pgpmime" && thread.Secure == "none") { update["secure"] = "some" } if err := gorethink.Db(config.RethinkDatabase).Table("threads").Get(thread.ID).Update(update).Exec(session); err != nil { return describeError(err) } } // Generate list of all owned emails ownEmails := map[string]struct{}{} for domain, _ := range domains { ownEmails[account.Name+"@"+domain] = struct{}{} } // Remove ownEmails from to and cc to2 := []string{} for _, value := range to { addr, err := mail.ParseAddress(value) if err != nil { // Mail is probably empty continue } if _, ok := ownEmails[addr.Address]; !ok { to2 = append(to2, value) } } to = to2 if cc != nil { cc2 := []string{} for _, value := range cc { addr, err := mail.ParseAddress(value) if err != nil { continue } if _, ok := ownEmails[addr.Address]; !ok { cc2 = append(cc2, value) } } cc = cc2 } // Prepare a new email es := &models.Email{ Resource: models.Resource{ ID: eid, DateCreated: time.Now(), DateModified: time.Now(), Name: subject, Owner: account.ID, }, Kind: kind, From: from, To: to, CC: cc, Body: body, Thread: thread.ID, MessageID: strings.Trim(email.Headers.Get("Message-ID"), "<>"), // todo: create a message id parser Status: "received", } if fileIDs != nil { es.Files = fileIDs[account.ID] } if manifest != "" { es.Manifest = manifest } // Insert the email if err := gorethink.Db(config.RethinkDatabase).Table("emails").Insert(es).Exec(session); err != nil { return describeError(err) } // Prepare a notification message notification, err := json.Marshal(map[string]interface{}{ "id": eid, "owner": account.ID, }) if err != nil { return describeError(err) } // Notify the cluster if err := producer.Publish("email_receipt", notification); err != nil { return describeError(err) } // Trigger the hooks hook, err := json.Marshal(&events.Incoming{ Email: eid, Account: account.ID, }) if err != nil { return describeError(err) } // Push it to nsq if err = producer.Publish("hook_incoming", hook); err != nil { return describeError(err) } log.WithFields(logrus.Fields{ "id": eid, }).Info("Finished processing an email") } return nil } }
// AccountsCreate creates a new account in the system. func AccountsCreate(w http.ResponseWriter, r *http.Request) { // Decode the request var input AccountsCreateRequest err := utils.ParseRequest(r, &input) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), }).Warn("Unable to decode a request") utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid input format", }) return } // TODO: Sanitize the username // TODO: Hash the password if it's not hashed already // Accounts flow: // 1) POST /accounts {username, alt_email} => status = registered // 2) POST /accounts {username, invite_code} => checks invite_code validity // 3) POST /accounts {username, invite_code, password} => status = setup requestType := "unknown" if input.Username != "" && input.Password == "" && input.AltEmail != "" && input.InviteCode == "" { requestType = "register" } else if input.Username != "" && input.Password == "" && input.AltEmail == "" && input.InviteCode != "" { requestType = "verify" } else if input.Username != "" && input.Password != "" && input.AltEmail == "" && input.InviteCode != "" { requestType = "setup" } // "unknown" requests are empty and invalid if requestType == "unknown" { utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid request", }) return } if requestType == "register" { // Normalize the username input.Username = utils.NormalizeUsername(input.Username) // Validate the username if len(input.Username) < 3 || len(utils.RemoveDots(input.Username)) < 3 || len(input.Username) > 32 { utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid username - it has to be at least 3 and at max 32 characters long", }) return } // Ensure that the username is not used in address table if used, err := env.Addresses.GetAddress(utils.RemoveDots(input.Username)); err == nil || used != nil { utils.JSONResponse(w, 409, &AccountsCreateResponse{ Success: false, Message: "Username already used", }) return } // Then check it in the accounts table if ok, err := env.Accounts.IsUsernameUsed(utils.RemoveDots(input.Username)); ok || err != nil { utils.JSONResponse(w, 409, &AccountsCreateResponse{ Success: false, Message: "Username already used", }) return } // Also check that the email is unique if used, err := env.Accounts.IsEmailUsed(input.AltEmail); err != nil || used { if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), }).Error("Unable to lookup registered accounts for emails") } utils.JSONResponse(w, 409, &AccountsCreateResponse{ Success: false, Message: "Email already used", }) return } // Both username and email are filled, so we can create a new account. account := &models.Account{ Resource: models.MakeResource("", utils.RemoveDots(input.Username)), StyledName: input.Username, Type: "beta", // Is this the proper value? AltEmail: input.AltEmail, Status: "registered", } // Try to save it in the database if err := env.Accounts.Insert(account); err != nil { utils.JSONResponse(w, 500, &AccountsCreateResponse{ Success: false, Message: "Internal server error - AC/CR/02", }) env.Log.WithFields(logrus.Fields{ "error": err.Error(), }).Error("Could not insert an user into the database") return } // TODO: Send emails here. Depends on @andreis work. // Return information about the account utils.JSONResponse(w, 201, &AccountsCreateResponse{ Success: true, Message: "Your account has been added to the beta queue", Account: account, }) return } else if requestType == "verify" { // We're pretty much checking whether an invitation code can be used by the user input.Username = utils.RemoveDots( utils.NormalizeUsername(input.Username), ) // Fetch the user from database account, err := env.Accounts.FindAccountByName(input.Username) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), "username": input.Username, }).Warn("User not found in the database") utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid username", }) return } // Fetch the token from the database token, err := env.Tokens.GetToken(input.InviteCode) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), }).Warn("Unable to fetch a registration token from the database") utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid invitation code", }) return } // Ensure that the invite code was given to this particular user. if token.Owner != account.ID { env.Log.WithFields(logrus.Fields{ "user_id": account.ID, "owner": token.Owner, }).Warn("Not owned invitation code used by an user") utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid invitation code", }) return } // Ensure that the token's type is valid if token.Type != "verify" { utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid invitation code", }) return } // Check if it's expired if token.Expired() { utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Expired invitation code", }) return } // Ensure that the account is "registered" if account.Status != "registered" { utils.JSONResponse(w, 403, &AccountsCreateResponse{ Success: true, Message: "This account was already configured", }) return } // Everything is fine, return it. utils.JSONResponse(w, 200, &AccountsCreateResponse{ Success: true, Message: "Valid token was provided", }) return } else if requestType == "setup" { // User is setting the password in the setup wizard. This should be one of the first steps, // as it's required for him to acquire an authentication token to configure their account. input.Username = utils.RemoveDots( utils.NormalizeUsername(input.Username), ) // Fetch the user from database account, err := env.Accounts.FindAccountByName(input.Username) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), "username": input.Username, }).Warn("User not found in the database") utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid username", }) return } // Fetch the token from the database token, err := env.Tokens.GetToken(input.InviteCode) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), }).Warn("Unable to fetch a registration token from the database") utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid invitation code", }) return } // Ensure that the invite code was given to this particular user. if token.Owner != account.ID { env.Log.WithFields(logrus.Fields{ "user_id": account.ID, "owner": token.Owner, }).Warn("Not owned invitation code used by an user") utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid invitation code", }) return } // Ensure that the token's type is valid if token.Type != "verify" { utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Invalid invitation code", }) return } // Check if it's expired if token.Expired() { utils.JSONResponse(w, 400, &AccountsCreateResponse{ Success: false, Message: "Expired invitation code", }) return } // Ensure that the account is "registered" if account.Status != "registered" { utils.JSONResponse(w, 403, &AccountsCreateResponse{ Success: true, Message: "This account was already configured", }) return } // Our token is fine, next part: password. // Ensure that user has chosen a secure password (check against 10k most used) if env.PasswordBF.TestString(input.Password) { utils.JSONResponse(w, 403, &AccountsCreateResponse{ Success: false, Message: "Weak password", }) return } // We can't really make more checks on the password, user could as well send us a hash // of a simple password, but we assume that no developer is that stupid (actually, // considering how many people upload their private keys and AWS credentials, I'm starting // to doubt the competence of some so-called "web deyvelopayrs") // Set the password err = account.SetPassword(input.Password) if err != nil { utils.JSONResponse(w, 500, &AccountsCreateResponse{ Success: false, Message: "Internal server error - AC/CR/01", }) env.Log.WithFields(logrus.Fields{ "error": err.Error(), }).Error("Unable to hash the password") return } account.Status = "setup" // Create labels err = env.Labels.Insert([]*models.Label{ &models.Label{ Resource: models.MakeResource(account.ID, "Inbox"), Builtin: true, }, &models.Label{ Resource: models.MakeResource(account.ID, "Sent"), Builtin: true, }, &models.Label{ Resource: models.MakeResource(account.ID, "Drafts"), Builtin: true, }, &models.Label{ Resource: models.MakeResource(account.ID, "Trash"), Builtin: true, }, &models.Label{ Resource: models.MakeResource(account.ID, "Spam"), Builtin: true, }, &models.Label{ Resource: models.MakeResource(account.ID, "Starred"), Builtin: true, }, }) if err != nil { utils.JSONResponse(w, 500, &AccountsCreateResponse{ Success: false, Message: "Internal server error - AC/CR/03", }) env.Log.WithFields(logrus.Fields{ "error": err.Error(), }).Error("Could not insert labels into the database") return } // Add a new mapping err = env.Addresses.Insert(&models.Address{ Resource: models.Resource{ ID: account.Name, DateCreated: time.Now(), DateModified: time.Now(), Owner: account.ID, }, }) if err != nil { utils.JSONResponse(w, 500, &AccountsCreateResponse{ Success: false, Message: "Unable to create a new address mapping", }) env.Log.WithFields(logrus.Fields{ "error": err.Error(), }).Error("Could not insert an address mapping into db") return } // Update the account err = env.Accounts.UpdateID(account.ID, account) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), "id": account.ID, }).Error("Unable to update an account") utils.JSONResponse(w, 500, &AccountsCreateResponse{ Success: false, Message: "Unable to update the account", }) return } // Remove the token and return a response err = env.Tokens.DeleteID(input.InviteCode) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), "id": input.InviteCode, }).Error("Could not remove the token from database") } utils.JSONResponse(w, 200, &AccountsCreateResponse{ Success: true, Message: "Your account has been initialized successfully", Account: account, }) return } }
func create(w http.ResponseWriter, req *http.Request) { // Decode the body var msg createInput err := json.NewDecoder(req.Body).Decode(&msg) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } // Fetch the invite from database cursor, err := r.Db(*rethinkName).Table("invites").Get(msg.Token).Run(session) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } var invite *Invite err = cursor.One(&invite) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } // Normalize the username styledName := msg.Username msg.Username = utils.RemoveDots(utils.NormalizeUsername(msg.Username)) var account *models.Account // If there's no account id, then simply check args if invite.AccountID == "" { if !govalidator.IsEmail(msg.Email) { writeJSON(w, errorMsg{ Success: false, Message: "Invalid email address", }) return } // Check if address is taken cursor, err = r.Db(*rethinkAPIName).Table("addresses").Get(msg.Username).Run(session) if err == nil || cursor != nil { writeJSON(w, freeMsg{ Success: false, UsernameTaken: true, }) return } // Check if email is used cursor, err = r.Db(*rethinkAPIName).Table("accounts"). GetAllByIndex("alt_email", msg.Email). Filter(r.Row.Field("id").Ne(r.Expr(invite.AccountID))). Count().Run(session) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } var emailCount int err = cursor.One(&emailCount) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } if emailCount > 0 { writeJSON(w, freeMsg{ Success: false, EmailUsed: true, }) return } // Prepare a new account account = &models.Account{ Resource: models.MakeResource("", msg.Username), AltEmail: msg.Email, StyledName: styledName, Status: "registered", Type: "supporter", } // Update the invite invite.AccountID = account.ID err = r.Db(*rethinkName).Table("invites").Get(invite.ID).Update(map[string]interface{}{ "account_id": invite.AccountID, }).Exec(session) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } // Insert the account into db err = r.Db(*rethinkAPIName).Table("accounts").Insert(account).Exec(session) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } } else { cursor, err = r.Db(*rethinkAPIName).Table("accounts").Get(invite.AccountID).Run(session) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } defer cursor.Close() if err := cursor.One(&account); err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } if account.Name != "" && account.Name != msg.Username { writeJSON(w, errorMsg{ Success: false, Message: "Invalid username", }) return } else if account.Name == "" { // Check if address is taken cursor, err = r.Db(*rethinkAPIName).Table("addresses").Get(msg.Username).Run(session) if err == nil || cursor != nil { writeJSON(w, errorMsg{ Success: false, Message: "Username is taken", }) return } } if account.AltEmail != "" && account.AltEmail != msg.Email { writeJSON(w, errorMsg{ Success: false, Message: "Invalid email", }) return } if account.AltEmail == "" { if !govalidator.IsEmail(msg.Email) { writeJSON(w, errorMsg{ Success: false, Message: "Invalid email address", }) return } // Check if email is used cursor, err = r.Db(*rethinkAPIName).Table("accounts"). GetAllByIndex("alt_email", msg.Email). Filter(r.Row.Field("id").Ne(r.Expr(invite.AccountID))). Count().Run(session) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } defer cursor.Close() var emailCount int err = cursor.One(&emailCount) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } if emailCount > 0 { writeJSON(w, errorMsg{ Success: false, Message: "Email is already used", }) return } } if err := r.Db(*rethinkAPIName).Table("accounts").Get(invite.AccountID).Update(map[string]interface{}{ "type": "supporter", }).Exec(session); err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } } // Generate a new invite token for the user token := &models.Token{ Resource: models.MakeResource(account.ID, "Invitation token from invite-api"), Type: "verify", Expiring: models.Expiring{ ExpiryDate: time.Now().UTC().Add(time.Hour * 12), }, } // Insert it into db err = r.Db(*rethinkAPIName).Table("tokens").Insert(token).Exec(session) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } // Here be dragons. Thou art forewarned. /*go func() { // Watch the changes cursor, err := r.Db(*rethinkAPIName).Table("accounts").Get(account.ID).Changes().Run(session) if err != nil { log.Print("Error while watching changes of user " + account.Name + " - " + err.Error()) return } defer cursor.Close() // Generate a timeout "flag" ts := uniuri.New() // Read them c := make(chan struct{}) go func() { var change struct { NewValue map[string]interface{} `gorethink:"new_val"` } for cursor.Next(&change) { if status, ok := change.NewValue["status"]; ok { if x, ok := status.(string); ok && x == "setup" { c <- struct{}{} return } } if iat, ok := change.NewValue["_invite_api_timeout"]; ok { if x, ok := iat.(string); ok && x == ts { log.Print("Account setup watcher timeout for name " + account.Name) return } } } }() // Block the goroutine select { case <-c: if err := r.Db(*rethinkName).Table("invites").Get(invite.ID).Delete().Exec(session); err != nil { log.Print("Unable to delete an invite. " + invite.ID + " - " + account.ID) return } return case <-time.After(12 * time.Hour): if err := r.Db(*rethinkAPIName).Table("accounts").Get(account.ID).Update(map[string]interface{}{ "_invite_api_timeout": ts, }).Exec(session); err != nil { log.Print("Failed to make a goroutine timeout. " + account.ID) } return } }()*/ // jk f**k that if err := r.Db(*rethinkName).Table("invites").Get(invite.ID).Delete().Exec(session); err != nil { log.Print("Unable to delete an invite. " + invite.ID + " - " + account.ID) return } // Return the token writeJSON(w, createMsg{ Success: true, Code: token.ID, }) }
// KeysGet does *something* - TODO func KeysGet(c web.C, w http.ResponseWriter, r *http.Request) { // Initialize vars var ( key *models.Key ) // Get ID from the passed URL params id := c.URLParams["id"] // Check if ID is an email or a fingerprint. // Fingerprints can't contain @, right? if strings.Contains(id, "@") { // Who cares about the second part? I don't! username := strings.Split(id, "@")[0] username = utils.RemoveDots(utils.NormalizeUsername(username)) // Resolve address address, err := env.Addresses.GetAddress(username) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), "name": username, }).Warn("Unable to fetch the requested address from the database") utils.JSONResponse(w, 404, &KeysGetResponse{ Success: false, Message: "No such address", }) return } // Get its owner account, err := env.Accounts.GetAccount(address.Owner) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), "name": username, }).Warn("Unable to fetch the requested account from the database") utils.JSONResponse(w, 404, &KeysGetResponse{ Success: false, Message: "No such account", }) return } // Does the user have a default PGP key set? if account.PublicKey != "" { // Fetch the requested key from the database key2, err := env.Keys.FindByFingerprint(account.PublicKey) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), }).Warn("Unable to fetch the requested key from the database") utils.JSONResponse(w, 500, &KeysGetResponse{ Success: false, Message: "Invalid user public key ID", }) return } key = key2 } else { keys, err := env.Keys.FindByOwner(account.ID) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), "owner": account.ID, }).Warn("Unable to fetch user's keys from the database") utils.JSONResponse(w, 500, &KeysGetResponse{ Success: false, Message: "Cannot find keys assigned to the account", }) return } if len(keys) == 0 { utils.JSONResponse(w, 500, &KeysGetResponse{ Success: false, Message: "Account has no keys assigned to itself", }) return } // i should probably sort them? key = keys[0] } } else { // Fetch the requested key from the database key2, err := env.Keys.FindByFingerprint(id) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), }).Warn("Unable to fetch the requested key from the database") utils.JSONResponse(w, 404, &KeysGetResponse{ Success: false, Message: "Requested key does not exist on our server", }) return } key = key2 } // Return the requested key utils.JSONResponse(w, 200, &KeysGetResponse{ Success: true, Key: key, }) }
// TokensCreate allows logging in to an account. func TokensCreate(w http.ResponseWriter, r *http.Request) { // Decode the request var input TokensCreateRequest err := utils.ParseRequest(r, &input) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), }).Warn("Unable to decode a request") utils.JSONResponse(w, 409, &TokensCreateResponse{ Success: false, Message: "Invalid input format", }) return } // We can only create "auth" tokens now if input.Type != "auth" { utils.JSONResponse(w, 409, &TokensCreateResponse{ Success: false, Message: "Only auth tokens are implemented", }) return } input.Username = utils.RemoveDots( utils.NormalizeUsername(input.Username), ) // Check if account exists user, err := env.Accounts.FindAccountByName(input.Username) if err != nil { utils.JSONResponse(w, 403, &TokensCreateResponse{ Success: false, Message: "Wrong username or password", }) return } // "registered" accounts can't log in if user.Status == "registered" { utils.JSONResponse(w, 403, &TokensCreateResponse{ Success: false, Message: "Your account is not confirmed", }) return } // Verify the password valid, updated, err := user.VerifyPassword(input.Password) if err != nil || !valid { utils.JSONResponse(w, 403, &TokensCreateResponse{ Success: false, Message: "Wrong username or password", }) return } // Update the user if password was updated if updated { user.DateModified = time.Now() err := env.Accounts.UpdateID(user.ID, user) if err != nil { env.Log.WithFields(logrus.Fields{ "user": user.Name, "error": err.Error(), }).Error("Could not update user") // DO NOT RETURN! } } // Check for 2nd factor if user.FactorType != "" { factor, ok := env.Factors[user.FactorType] if ok { // Verify the 2FA verified, challenge, err := user.Verify2FA(factor, input.Token) if err != nil { utils.JSONResponse(w, 500, &TokensCreateResponse{ Success: false, Message: "Internal 2FA error", }) env.Log.WithFields(logrus.Fields{ "err": err.Error(), "factor": user.FactorType, }).Warn("2FA authentication error") return } // Token was probably empty. Return the challenge. if !verified && challenge != "" { utils.JSONResponse(w, 403, &TokensCreateResponse{ Success: false, Message: "2FA token was not passed", FactorType: user.FactorType, FactorChallenge: challenge, }) return } // Token was incorrect if !verified { utils.JSONResponse(w, 403, &TokensCreateResponse{ Success: false, Message: "Invalid token passed", FactorType: user.FactorType, }) return } } } // Calculate the expiry date expDate := time.Now().Add(time.Hour * time.Duration(env.Config.SessionDuration)) // Create a new token token := &models.Token{ Expiring: models.Expiring{ExpiryDate: expDate}, Resource: models.MakeResource(user.ID, "Auth token expiring on "+expDate.Format(time.RFC3339)), Type: input.Type, } // Insert int into the database env.Tokens.Insert(token) // Respond with the freshly created token utils.JSONResponse(w, 201, &TokensCreateResponse{ Success: true, Message: "Authentication successful", Token: token, }) }
func free(w http.ResponseWriter, req *http.Request) { // Decode the POST body var msg freeInput err := json.NewDecoder(req.Body).Decode(&msg) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } // Fetch the invite from database cursor, err := r.Db(*rethinkName).Table("invites").Get(msg.Token).Run(session) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } var invite *Invite err = cursor.One(&invite) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } // Normalize the username - make it lowercase and remove dots msg.Username = utils.RemoveDots(utils.NormalizeUsername(msg.Username)) if invite.AccountID != "" { // Fetch account from database cursor, err := r.Db(*rethinkAPIName).Table("accounts").Get(invite.AccountID).Run(session) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } var account *models.Account if err := cursor.One(&account); err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } if account.Name != "" && account.Name != msg.Username { writeJSON(w, errorMsg{ Success: false, Message: "Invalid username", }) return } if account.AltEmail != "" && account.AltEmail != msg.Email { writeJSON(w, errorMsg{ Success: false, Message: "Invalid email", }) return } if account.AltEmail == "" && !govalidator.IsEmail(msg.Email) { writeJSON(w, errorMsg{ Success: false, Message: "Invalid email address", }) return } if account.Name == "" { // Check if address is taken cursor, err = r.Db(*rethinkAPIName).Table("addresses").Get(msg.Username).Run(session) if err == nil || cursor != nil { writeJSON(w, freeMsg{ Success: false, UsernameTaken: true, }) return } } if account.AltEmail == "" { // Check if email is used cursor, err = r.Db(*rethinkAPIName).Table("accounts"). GetAllByIndex("alt_email", msg.Email).Count().Run(session) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } var emailCount int err = cursor.One(&emailCount) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } if emailCount > 0 { writeJSON(w, freeMsg{ Success: false, EmailUsed: true, }) return } } // Return the result writeJSON(w, freeMsg{ Success: true, }) } else { if !govalidator.IsEmail(msg.Email) { writeJSON(w, errorMsg{ Success: false, Message: "Invalid email address", }) return } // Check if address is taken cursor, err = r.Db(*rethinkAPIName).Table("addresses").Get(msg.Username).Run(session) if err == nil || cursor != nil { writeJSON(w, freeMsg{ Success: false, UsernameTaken: true, }) return } // Check if email is used cursor, err = r.Db(*rethinkAPIName).Table("accounts"). GetAllByIndex("alt_email", msg.Email). Filter(r.Row.Field("id").Ne(r.Expr(invite.AccountID))). Count().Run(session) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } var emailCount int err = cursor.One(&emailCount) if err != nil { writeJSON(w, errorMsg{ Success: false, Message: err.Error(), }) return } if emailCount > 0 { writeJSON(w, freeMsg{ Success: false, EmailUsed: true, }) return } // Return the result writeJSON(w, freeMsg{ Success: true, }) } }