3 # Copyright (C) 2016 Ian Kelling
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 # debian sets LANG=C when starting apache2.
19 # the envoding comment above fixes the internal encoding afaik,
21 # https://stackoverflow.com/questions/20521371/set-utf-8-as-default-for-ruby-1-9-3
22 # also note man ruby's -E arg.
23 Encoding
.default_external
= Encoding
::UTF_8
30 Dir
.chdir(File
.join(File
.dirname(__FILE__
), '..'))
40 x
= (<<EOF).split("\n")
43 Would you sleep better on a bed or a keyboard?
45 Are there more or less than one million people on the Earth?
47 Which of Lilly and Robert is more commonly a woman’s name?
49 Which word has fewer letters, adorable or fox?
51 What is the normal color of milk?
53 Which of Iceland and Turkey is an island nation?
55 Is a filesystem like a tree or a rose?
57 What character is a tilde?
59 Which is brighter, the moon or the sun?
61 Which is closer, the moon or the sun?
63 What language is this sentence written in?
65 What animal says "meow" and catches mice?
67 What animal quacks and has webbed feet?
69 Which are better fliers: worms or birds?
71 Which typically runs first: a kernel or a web browser?
81 captcha_q
= CAPTCHA
.sample
[0]
82 puts
"Content-type: text/html\n\n"
83 puts
skel("#{DN}/captcha", <<EOF, header: ' / <a href="/blog.html">blog</a> / captcha')
84 <p>Hello friend. I haven't read a post from #{IP}, and I only remember for a few months, so:</p>
87 <form action="/cgi/comment" method="post">
88 <input class="misc" type="text" name="url">
89 <input name="goto" type="hidden" value="#{GOTO}">
90 <input name="question" type="hidden" value="#{captcha_q}">
93 <textarea rows="10" name="comment" maxlength="1000">#{COMMENT_TXT}</textarea>
94 <input type="submit" value="Submit">
103 puts
"Content-type: text/plain\n\n"
112 puts
'Status: 302 Found'
113 puts
"Location: #{GOTO}#comment-section\n\n"
123 ###### begin error checking & arg parsing ######
127 if cgi
.has_key
?('goto')
134 if (cgi
.has_key
?('url') && cgi
['url'] != "") || ! cgi
.has_key
?('comment')
135 fail("comment not in form or url in form. cgi.params: #{cgi.params}")
138 COMMENT_TXT
= cgi
["comment"]
141 if COMMENT_TXT
.length
> 1000 or GOTO
.length
> 150
142 fail('length of comment or goto is too great')
145 if COMMENT_TXT
.length
<= 2 or COMMENT_TXT
=~
/\A\s*\Z/
146 fail('not enough content in comment')
151 if cgi
.has_key
?('answer') && cgi
.has_key
?('question')
152 if cgi
['answer'].downcase
!~
/^#{CAPTCHA.to_h[cgi['question']]}$/
161 Dir
.foreach('blog') do |entry
|
162 next if ['.','..'].any
? { |f
| f
== entry
}
163 if GOTO
== '/blog/' + entry
168 fail('goto entry not found') unless found
170 ######### end error checking & arg parsing ########
175 WHITELIST_CUTOFF
= NOW
- 4*DAY
178 ####### begin: state for ips we've seen before #######
181 [20, 60*60], # 60 min
182 [30, 60*60*24], # 1 day
183 [60, 60*60*24*7]] # 1 week
184 .each
do |max_posts
, date
|
186 if $db.execute(<<-SQL, [NOW - date])[0][0] > max_posts
187 select count(*) from c
188 where date > ? and ip = '#{IP}'
190 state
= 'rate_limited'
194 state
||= 'suspect' if $db.execute(<<-SQL)[0][0] > 0
195 select count(*) from c
196 where ip = '#{IP}' and (
198 state = 'rate_limited')
202 older_date
= NOW
- DAY
*2
203 last_moderated
= $db.execute(<<-SQL, [older_date])[-1]
205 where ip = '#{IP}' and (
206 state = 'moderated' or
207 (date < ? and (state = 'timed' or state = 'known')))
209 last_moderated
= last_moderated
[0] if last_moderated
210 last_good
= $db.execute(<<-SQL, [older_date])[-1]
212 where ip = '#{IP}' and (
214 (date < ? and (state = 'timed' or state = 'known')))
216 last_good
= last_good
[0] if last_good
217 if last_moderated
&& last_good
218 if last_good
> last_moderated
221 # these 2 waiting conditions are not actually needed,
222 # since waiting is the default, but meh.
231 ####### end: state for ips we've seen before #######
233 ####### begin: whitelist checking #########
234 glob
= "../blog/#{'?'*'YYYY-MM-DD-'.length}#{bn GOTO, '.*'}.md"
235 md_file
= Dir
[glob
][0]
238 post_date
= Time
.parse(b
[0..DATE_LEN
]).to_i
239 if post_date
> WHITELIST_CUTOFF
243 ###### end: whitelist checking ########
247 if state
!= 'known' && ! captchad
254 # # was posted a whitelist period, so automatically posted.
255 # # whitelist periods are per page times when legit comments are
256 # # much more likely than spam, so we automatically let comments through.
259 # # ip posted good comment before: either, one in picked state, or
260 # # a timed/known comment which is over 2 days old (I saw it and didn't remove
264 # # manually marked as a good comment, so publish it.
267 # # posting too much, consider them a spammer.
270 # # bad comment, but don't ban them
273 # # all comments from this ip dead, new comment's dont even go into the db.
276 # # waiting for manual moderation, get's posted automatically if there
277 # # is none in 24 hours
280 # # had a bad post in the past. does not
281 # # automatically get posted in time without moderation.
284 # any of the manual states
285 date
= $db.execute(<<-SQL)[0][0]
286 select max(date) from c where
287 state = 'moderated' or
292 # not the bad automatic states
294 select count(*) from c where
295 state != 'rate_limited' and
299 $db.execute('insert into c values (NULL, ?, ?, ?, ?, ?)',
307 new_count
= $db.execute(query
+ 'and date > ?', date
)
309 new_count
= $db.execute(query
)
311 new_count
= new_count
[0][0]
315 def send_email(opts
={})
317 opts
[:server] ||= 'localhost'
318 opts
[:from] ||= 'root'
319 opts
[:from_alias] ||= 'root'
320 opts
[:subject] ||= "test subject"
323 msg
= <<END_OF_MESSAGE
324 From: #{opts[:from_alias]} <#{opts[:from]}>
326 Subject: #{opts[:subject]}
331 Net
::SMTP.start(opts
[:server]) do |smtp
|
332 smtp
.send_message msg
, opts
[:from], opts
[:to]
335 send_email
:subject => "new comment on #{DN}"