root/db/db.cc

Revision 2114058982a08f59f0f312544a16250eceab0098, 22.5 KB (checked in by Antti-Juhani Kaijanaho <antti-juhani@…>, 17 months ago)

#14: Support moderators with kill/mark-as-spam and resurrection powers

Signed-off-by: Antti-Juhani Kaijanaho <antti-juhani@…>

  • Property mode set to 100644
Line 
1/*  This file is part of Alue, the multiprotocol Internet discussion daemon
2
3    Copyright © 2009, 2010, 2011 Antti-Juhani Kaijanaho
4
5    Alue is free software: you can redistribute it and/or modify it
6    under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    Alue is distributed in the hope that it will be useful, but
11    WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13    General Public License for more details.
14
15    You should have received a copy of the GNU General Public License
16    along with Alue.  If not, see <http://www.gnu.org/licenses/>.
17
18 */
19
20#include "db.hh"
21#include "db_reader.hh"
22#include "msg.hh"
23#include "role.hh"
24#include "role_exists.hh"
25#include "user.hh"
26#include "user_exists.hh"
27
28#include "../config.hh"
29#include "../logger/logline.hh"
30#include "../msg/entity.hh"
31#include "../msg/util.hh"
32#include "../myassert.hh"
33#include "../util.hh"
34
35#include <boost/date_time/posix_time/posix_time.hpp>
36#include <sstream>
37#include <fstream>
38
39namespace db
40{
41        db::db()
42                : dbname(config["db-file"].as<std::string>())
43                , pwh(new password_handler(config["pw-file"].as<std::string>()))
44                , global_next(0)
45        {
46                register_role(role::anon());
47                register_role(role::authn());
48                register_role(role::poster());
49        }
50
51        void db::register_role(role::ptr r)
52        {
53                boost::lock_guard<boost::recursive_mutex> lg(mt);
54                if (roles[r->get_name()])
55                        throw role_exists(r->get_name());
56                roles[r->get_name()] = r;
57        }
58
59        boost::shared_ptr<role> db::create_role(std::string name,
60                                                std::string description)
61        {
62                boost::lock_guard<boost::recursive_mutex> lg(mt);
63                role::ptr rv(new role(name,description));
64                register_role(rv);
65                add_record("ROLE " + name + "\r\n" +
66                           (description.empty() ? "" :
67                            "DESCRIPTION " + description));
68                return rv;
69        }
70
71        void db::do_newgroup(db_reader &dr, std::string line,
72                             boost::posix_time::ptime timestamp)
73        {
74                std::string grp = line;
75                util::strip(grp);
76                std::string descr = dr.readline(line, "DESCRIPTION");
77                std::string readperms = dr.readline(line, "READING");
78                util::strip(readperms);
79                bool readperm;
80                if (readperms == "PERMITTED") readperm = false;
81                else if (readperms == "RESTRICTED") readperm = true;
82                else throw illformed_db("READING argument not recognised");
83                dr.endrec(line);
84                groups[grp].reset(new group(grp, shared_from_this(),
85                                            timestamp, readperm, descr));
86        }
87
88        void db::do_user(db_reader &dr, std::string line)
89        {
90                std::string userid = line;
91                util::strip(userid);
92                boost::shared_ptr<user> &u = users[userid];
93                if (!u) u.reset(new user(userid, shared_from_this(), pwh));
94                while (true)
95                {
96                        dr.readline(line);
97                        if (line == ".END" || line == ".END\r") break;
98                        std::string key = util::split(line);
99                        util::strip(line);
100                        if (key == "display_name")
101                                u->display_name = line;
102                        else if (key =="display_email")
103                                u->display_email = line;
104                        else if (key =="delivery_email")
105                        {
106                                u->delivery_email = line;
107                                users_email[line] = u;
108                        }
109                        else if (key =="delivery_email_verified")
110                                u->delivery_email_verified = line == "yes";
111                        else if (key =="delivery_email_cookie")
112                                u->delivery_email_cookie = line;
113                        else if (key == "allow_cleartext_password")
114                                u->do_allow_cleartext_password = line == "yes";
115                        else if (key == "has_read")
116                                u->read_msgids.insert(line);
117                        else if (key == "has_not_read")
118                                u->read_msgids.erase(line);
119                        else if (key == "allow_posting")
120                        {
121                                if (line == "yes")
122                                        u->roles.insert(role::poster());
123                                else
124                                        u->roles.erase(role::poster());
125                        }
126                        else
127                                throw illformed_db("user key" + key);
128                }
129        }
130
131        void db::do_role(db_reader &dr, std::string line)
132        {
133                std::string roleid = line;
134                util::strip(roleid);
135                if (roleid == role::authn()->get_name() ||
136                    roleid == role::anon()->get_name())
137                        throw illformed_db("reserved role in role record");
138                role::ptr &r = roles[roleid];
139                if (!r) r.reset(new role(roleid, ""));
140                while (true)
141                {
142                        dr.readline(line);
143                        if (line == ".END" || line == ".END\r") break;
144                        std::string key = util::split(line);
145                        if (key == "DESCRIPTION")
146                        {
147                                if (roleid == role::poster()->get_name())
148                                        throw illformed_db
149                                                ("bad use of 'poster'");
150                                if (role::is_subscriber_role(roleid))
151                                        throw illformed_db
152                                                ("bad use of 'subscribers:'");
153                                r->description = line;
154                        }
155                        else if (key == "USER")
156                        {
157                                std::string key2 = util::split(line);
158                                util::strip(line);
159                                bool add;
160                                if (key2 == "ADD")
161                                        add = true;
162                                else if (key2 == "DEL")
163                                        add = false;
164                                else
165                                        throw illformed_db("role user key");
166                                boost::shared_ptr<user> u = users[line];
167                                if (!u) throw illformed_db("no such user");
168                                if (add)
169                                {
170                                        u->roles.insert(r);
171                                        r->users.insert(u);
172                                }
173                                else
174                                {
175                                        u->roles.erase(r);
176                                        r->users.erase(u);
177                                }
178                        }
179                        else
180                                throw illformed_db("role key");
181                }
182        }
183
184        void db::do_moderation(db_reader &dr, std::string line)
185        {
186                std::string uid = line;
187                util::strip(uid);
188                user::ptr u = users[uid];
189                if (!u) throw illformed_db("MODERATION user invalid: " + uid);
190                while (true)
191                {
192                        dr.readline(line);
193                        if (line == ".END" || line == ".END\r") break;
194                        std::string keys = util::split(line);
195                        std::string mid = util::split(line);
196                        std::string reason = line;
197                        msg::ptr m = msgid[mid];
198                        if (!m)
199                        {
200                                logger::logline ll;
201                                ll << "unknown message in MODERATION ignored: "
202                                   << keys << " " << mid;
203                                continue;
204                        }
205                        msg::moderation::kind_type key;
206                        /**/ if (keys == "KILL")  key = msg::moderation::KILL;
207                        else if (keys == "SPAM")  key = msg::moderation::SPAM;
208                        else if (keys == "CLEAR") key = msg::moderation::CLEAR;
209                        else throw illformed_db("MODERATION key error: " +
210                                                keys);
211                        m->moderate(msg::moderation(key, u, reason));
212                }
213        }
214
215        void db::do_article(db_reader &dr, std::string line)
216        {
217                std::string msgid = line;
218                util::strip(msgid);
219                if (msgid.empty() || msgid[0] != '<' ||
220                    msgid[msgid.length()-1] != '>')
221                        throw illformed_db("invalid msgid");
222               
223                dr.readline(line);
224                user::ptr u;
225                if (line.substr(0, 10) == "POSTED BY ")
226                {
227                        std::string uid = line.substr(10);
228                        util::strip(uid);
229                        u = users[uid];
230                        BOOST_ASSERT(u);
231                        dr.readline(line);
232                }
233
234                std::map<std::string, group::number> xref;
235                while (true)
236                {
237                        if (line.substr(0, 8) != "FILE AS ") break;
238                        std::string fileas = line.substr(8);
239                        util::strip(fileas);
240                        size_t colon = fileas.find(':');
241                        if (colon == std::string::npos)
242                                throw illformed_db("invalid FILE AS");
243                        std::string grp = fileas.substr(0, colon);
244                        std::string nrs = fileas.substr(colon+1);
245                        group::number nr =
246                                boost::lexical_cast<group::number>(nrs);
247                        xref[grp] = nr;
248                        if (groups.find(grp) == groups.end())
249                                throw illformed_db("no such group " + grp);
250                        dr.readline(line);
251                }
252                util::strip_crlf(line);
253
254                if (line != "FOLLOWS") throw illformed_db("FOLLOWS missing");
255                std::ostringstream ss;
256                while (true)
257                {
258                        dr.readline(line);
259                        if (line == ".END" || line == ".END\r") break;
260                        if (!dr.valid()) throw rollback();
261                        ss << line;
262                        ss << '\n';
263                }
264               
265                std::string msgstr =
266                        ::msg::dot_destuff(::msg::crlf_canonize(ss.str()));
267
268                // commit
269                ::msg::entity::ptr entity = ::msg::entity::mk(msgstr, false);
270                msg::ptr message(new msg::msg(msgid,entity,u));
271                for (std::map<std::string, group::number>::const_iterator it =
272                             xref.begin();
273                     it != xref.end(); it++)
274                {
275                        std::string ng = it->first;
276                        group::number n = it->second;
277                        boost::shared_ptr<group> gr = groups[ng];
278                        gr->file(message, n);
279                }
280                if (this->msgid.find(msgid) != this->msgid.end())
281                {
282                        logger::logline ll;
283                        ll << "warning: duplicate message-id "
284                           << msgid
285                           << " in db";
286                }
287                this->msgid[msgid] = message;
288                thr.add_msg(global_next, message);
289                global_next++;
290        }
291
292        void db::read_record(db_reader &dr, boost::posix_time::ptime timestamp)
293        {
294                std::string line;
295                dr.readline(line);
296                std::string cmd = util::split(line);
297                if (cmd == "NEWGROUP")
298                        do_newgroup(dr, line, timestamp);
299                else if (cmd == "USER")
300                        do_user(dr, line);
301                else if (cmd == "ROLE")
302                        do_role(dr, line);
303                else if (cmd == "ARTICLE")
304                        do_article(dr, line);
305                else if (cmd == "MODERATION")
306                        do_moderation(dr, line);
307                else
308                        throw illformed_db("unrecognized record type " + cmd);
309        }
310
311        void db::init()
312        {               
313                boost::lock_guard<boost::recursive_mutex> lg(mt);
314
315                std::ifstream dbfile(dbname.c_str());
316                db_reader dr(dbfile);
317
318                while (true)
319                {
320                        std::string line;
321               
322                        std::getline(dbfile, line);
323                        if (dbfile.eof()) break;
324                        util::strip(line);
325                        if (line.substr(0, 7) != ".BEGIN ")
326                                throw illformed_db(".BEGIN missing");
327                               
328                        util::strip_crlf(line);
329                       
330                        boost::posix_time::ptime timestamp =
331                                boost::posix_time::from_iso_string
332                                (line.substr(7));
333                        try
334                        {
335                                read_record(dr, timestamp);
336                        }
337                        catch (illformed_db &id)
338                        {
339                                logger::logline ll;
340                                ll << "warning: " << id.what();
341                                while (dr.valid())
342                                {
343                                        dr.readline(line);
344                                        util::strip(line);
345                                        if (line == ".END") break;
346                                }
347                        }
348                        catch (rollback)
349                        {
350                                logger::logline ll;
351                                ll << "warning: rolled back a record";
352                        }
353                }
354                thr.recalc_roots();
355                thr.sort();
356        }
357
358        boost::shared_ptr<msg> db::file(boost::shared_ptr< ::msg::entity > ent,
359                                        boost::asio::ip::address source,
360                                        boost::shared_ptr<user> poster,
361                                        server::conn_cb cb)
362        {
363                // first, perform USEPRO-14 (USEFOR-12) injector duties
364
365                ::msg::complete_netnews(ent);
366
367                std::string server_name =
368                        config["canonical-name"].as<std::string>();
369
370                // FIXME checking existing Injection-Date and Date headers
371                // for being relatively current
372               
373                {
374                        std::ostringstream ss;
375                        ss << server_name
376                           << "; posting_host=\"" 
377                           << source.to_string()
378                           << "\"";
379                        ent->replace_field("Injection-Info", ss.str());
380                }
381
382                std::string path = ent->get_field("Path", false);
383                util::strip_crlf(path);
384                if (path.empty())
385                {
386                        path = "not-for-mail";
387                }
388                else
389                {
390                        if (path.find("!.POSTED") != std::string::npos)
391                        {
392                                throw filing_exception("Path contains "
393                                                       "a POSTED diag-keyword");
394                        }
395                }
396                path = server_name +
397                        " !.POSTED." + source.to_string() + " !" + path;
398                ent->replace_field("Path", path);
399
400                ::msg::validate_netnews(ent);
401
402                if (ent->has_field("Control"))
403                        throw filing_exception("Control messages "
404                                               "are not supported");
405                if (ent->has_field("Supersedes"))
406                        throw filing_exception("Supersession is not supported");
407
408                msg::ptr mg(new msg(ent, poster));
409
410                boost::lock_guard<boost::recursive_mutex> lg(mt);
411
412                std::list<std::string> ngs = mg->get_newsgroups();
413                const std::set<role::ptr> *rdset = 0;
414                bool have_groups = false;
415                for (std::list<std::string>::iterator it = ngs.begin();
416                     it != ngs.end(); it++)
417                {
418                        std::string ng = *it;
419                        boost::shared_ptr<group> gr = groups[ng];
420                        if (gr)
421                        {
422                                if (!rdset)
423                                {
424                                        rdset = &gr->get_readers();
425                                }
426                                else
427                                {
428                                        if (*rdset != gr->get_readers())
429                                                throw filing_exception("crossposting between differently reading-restricted groups is not allowed");
430                                }
431                                if (!gr->posting_authz(poster))
432                                        throw filing_exception("you are not authorized to post to " + gr->name());
433                                have_groups = true;
434                        }
435                }
436                if (!have_groups) throw filing_exception("none of our groups");
437
438                if (this->msgid.find(mg->msgid()) != this->msgid.end())
439                {
440                        logger::logline ll;
441                        ll << "duplicate message ID "
442                           << mg->msgid()
443                           << " offered by "
444                           << source;
445                        throw filing_exception("duplicate message ID");
446                }
447
448                // then do the actual filing
449
450                std::map<std::string, group::number> xref;
451                for (std::list<std::string>::iterator it = ngs.begin();
452                     it != ngs.end(); it++)
453                {
454                        std::string ng = *it;
455                        boost::shared_ptr<group> gr = groups[ng];
456                        if (gr)
457                        {
458                                group::number n = gr->file(mg, cb);
459                                xref[ng] = n;
460                        }
461                }
462
463                msgid[mg->msgid()] = mg;
464                thr.add_msg(global_next, mg);
465                thr.recalc_roots();
466                thr.sort();
467                global_next++;
468
469                std::ofstream dbfile(dbname.c_str(), std::ios_base::app);
470                dbfile << ".BEGIN "
471                       << boost::posix_time::to_iso_string(
472                               boost::posix_time::second_clock::universal_time()
473                               )
474                       << "\n"
475                       << "ARTICLE "
476                       << mg->msgid()
477                       << "\n"
478                       << "POSTED BY "
479                       << poster->get_userid()
480                       << "\n";
481                for (std::map<std::string, group::number>::const_iterator it
482                             = xref.begin();
483                     it != xref.end(); it++)
484                {
485                        dbfile << "FILE AS "
486                               << it->first
487                               << ":"
488                               << it->second
489                               << "\n";
490                }
491                dbfile << "FOLLOWS\n"
492                       << ::msg::dot_stuff(mg->get_entity()->get_entity())
493                       << ".END\n";
494                dbfile.close();
495                return mg;
496        }
497
498        void db::add_record(std::string record)
499        {
500                if (record.empty()) return;
501
502                boost::lock_guard<boost::recursive_mutex> lg(mt);
503
504                for (size_t i = 0; i < record.length(); i++)
505                {
506                        if (record[i] != '.') continue;
507                        if (i > 0 && record[i-1] != '\n' && record[i-1] != '\r')
508                                continue;
509                        record.insert(i, ".");
510                }
511                if (record[record.length() - 1] != '\n' &&
512                    record[record.length() - 1] != '\r')
513                        record += '\n';
514                record += ".END\n";
515
516                boost::posix_time::ptime timestamp =
517                        boost::posix_time::second_clock::universal_time();
518
519                std::ofstream dbfile(dbname.c_str(), std::ios_base::app);
520                dbfile << ".BEGIN "
521                       << boost::posix_time::to_iso_string(timestamp)
522                       << "\n"
523                       << record;
524                dbfile.close();
525                std::istringstream ss(record);
526                db_reader dr(ss);
527                read_record(dr, timestamp);
528        }
529
530        boost::shared_ptr<user> db::create_user(std::string userid,
531                                                std::string passwd)
532        {
533                boost::lock_guard<boost::recursive_mutex> lg(mt);
534                boost::shared_ptr<user> &rv = users[userid];
535                if (rv) throw user_exists(userid);
536                rv.reset(new user(userid, shared_from_this(), pwh));
537                rv->set_password(passwd);
538
539                add_record(std::string("USER ") + userid);
540
541                rv->roles.insert(role::poster());
542                add_record(std::string("ROLE poster\nUSER ADD ") + userid);
543
544                logger::logline ll;
545                ll << "user '" << userid << "' created";
546               
547                return rv;
548        }
549}
Note: See TracBrowser for help on using the browser.