#!/usr/bin/env ruby # encoding: utf-8 # Copyright (C) 2016 Ian Kelling # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # debian sets LANG=C when starting apache2. # the envoding comment above fixes the internal encoding afaik, # Found this at # https://stackoverflow.com/questions/20521371/set-utf-8-as-default-for-ruby-1-9-3 # also note man ruby's -E arg. Encoding.default_external = Encoding::UTF_8 require 'cgi' require 'fileutils' require 'time' require 'sqlite3' Dir.chdir(File.join(File.dirname(__FILE__), '..')) require '../b' include B # constanty things DEBUG = true CAPTCHA = -> { c = [] x = (< 0 c << x.pop(2) end c }[] def do_captcha captcha_q = CAPTCHA.sample[0] puts "Content-type: text/html\n\n" puts skel("#{DN}/captcha", <blog / captcha')

Hello friend. I haven't read a post from #{IP}, and I only remember for a few months, so:

#{captcha_q}


Your comment:
EOF exit 0 end def fail(msg) if DEBUG and msg puts "Content-type: text/plain\n\n" puts msg else redir end exit 0 end def redir puts 'Status: 302 Found' puts "Location: #{GOTO}#comment-section\n\n" exit(0) end def bn(*args) File.basename *args end ###### begin error checking & arg parsing ###### cgi = CGI.new IP = cgi.remote_addr if cgi.has_key?('goto') GOTO = cgi['goto'] else GOTO = '/' fail('redir to /') end if (cgi.has_key?('url') && cgi['url'] != "") || ! cgi.has_key?('comment') fail("comment not in form or url in form. cgi.params: #{cgi.params}") end COMMENT_TXT = cgi["comment"] if COMMENT_TXT.length > 1000 or GOTO.length > 150 fail('length of comment or goto is too great') end captchad = false if cgi.has_key?('answer') && cgi.has_key?('question') if cgi['answer'].downcase !~ /^#{CAPTCHA.to_h[cgi['question']]}$/ do_captcha end captchad = true end -> { found = false Dir.foreach('blog') do |entry| next if ['.','..'].any? { |f| f == entry } if GOTO == '/blog/' + entry found = true break end end fail('goto entry not found') unless found }[] ######### end error checking & arg parsing ######## $db = db_init state = nil WHITELIST_CUTOFF = NOW - 4*DAY ####### begin: state for ips we've seen before ####### [[5, 60], # 1 min [10, 60*5], # 5 min [20, 60*60], # 60 min [30, 60*60*24], # 1 day [60, 60*60*24*7]] # 1 week .each do |max_posts, date| if $db.execute(<<-SQL, [NOW - date])[0][0] > max_posts select count(*) from c where date > ? and ip = '#{IP}' SQL state = 'rate_limited' end end state ||= 'suspect' if $db.execute(<<-SQL)[0][0] > 0 select count(*) from c where ip = '#{IP}' and ( state = 'banned' or state = 'rate_limited') SQL unless state older_date = NOW - DAY*2 last_moderated = $db.execute(<<-SQL, [older_date])[-1] select date from c where ip = '#{IP}' and ( state = 'moderated' or (date < ? and (state = 'timed' or state = 'known'))) SQL last_moderated = last_moderated[0] if last_moderated last_good = $db.execute(<<-SQL, [older_date])[-1] select date from c where ip = '#{IP}' and ( state = 'picked' or (date < ? and (state = 'timed' or state = 'known'))) SQL last_good = last_good[0] if last_good if last_moderated && last_good if last_good > last_moderated state = 'known' else # these 2 waiting conditions are not actually needed, # since waiting is the default, but meh. state = 'waiting' end elsif last_moderated state = 'waiting' elsif last_good state = 'known' end end ####### end: state for ips we've seen before ####### ####### begin: whitelist checking ######### glob = "../blog/#{'?'*'YYYY-MM-DD-'.length}#{bn GOTO, '.*'}.md" md_file = Dir[glob][0] unless state b = bn(md_file,'.*') post_date = Time.parse(b[0..DATE_LEN]).to_i if post_date > WHITELIST_CUTOFF state = 'timed' end end ###### end: whitelist checking ######## state ||= 'waiting' if state != 'known' && ! captchad do_captcha end # states: # timed # # was posted a whitelist period, so automatically posted. # # whitelist periods are per page times when legit comments are # # much more likely than spam, so we automatically let comments through. # known # # ip posted good comment before: either, one in picked state, or # # a timed/known comment which is over 2 days old (I saw it and didn't remove # # it) # picked # # manually marked as a good comment, so publish it. # rate_limited # # posting too much, consider them a spammer. # moderated # # bad comment, but don't ban them # banned # # all comments from this ip dead, new comment's dont even go into the db. # waiting # # waiting for manual moderation, get's posted automatically if there # # is none in 24 hours # suspect # # had a bad post in the past. does not # # automatically get posted in time without moderation. # any of the manual states date = $db.execute(<<-SQL)[0][0] select max(date) from c where state = 'moderated' or state = 'banned' or state = 'picked' SQL # not the bad automatic states query = <<-SQL select count(*) from c where state != 'rate_limited' and state != 'suspect' SQL if date new_count = $db.execute(query + 'and date > ?',date) else new_count = $db.execute(query) end if new_count == 1 require 'net/smtp' def send_email(opts={}) opts[:to] ||= ENV['USER'] opts[:server] ||= 'localhost' opts[:from] ||= ENV['USER'] opts[:from_alias] ||= ENV['USER'] opts[:subject] ||= "test subject" opts[:body] ||= "" msg = < To: <#{opts[:to]}> Subject: #{opts[:subject]} #{opts[:body]} END_OF_MESSAGE Net::SMTP.start(opts[:server]) do |smtp| smtp.send_message msg, opts[:from], opts[:to] end end send_email :subject => 'new comments on iankelling.org' end $db.execute('insert into c values (NULL, ?, ?, ?, ?, ?)', [state, IP, NOW, GOTO, COMMENT_TXT]) post(md_file) redir