# frozen_string_literal: true require_relative './environment.rb' def app Sinatra::Application end describe Site do describe 'username' do it 'only passes valid hostnames for username' do _(Site.valid_username?('|\|0p|E')).must_equal false _(Site.valid_username?('nope-')).must_equal false _(Site.valid_username?('-nope')).must_equal false _(Site.valid_username?('do-pe')).must_equal true _(Site.valid_username?('d')).must_equal true _(Site.valid_username?('do')).must_equal true end end describe 'child sites' do it 'child sites do not require email validation' do parent_site = Fabricate :site, email: 'parent@example.com', email_confirmed: false child_site = Fabricate :site, parent_site_id: parent_site.id, email: nil _(parent_site.parent?).must_equal true _(child_site.parent?).must_equal false # Parent site needs email validation, child site does not _(parent_site.email_not_validated?).must_equal true _(child_site.email_not_validated?).must_equal false end end describe 'file type validation' do it 'allows css files when libmagic misidentifies them as appleworks' do _(Site.valid_file_mime_type_and_ext?('application/x-appleworks3', '.css')).must_equal true end it 'keeps the appleworks exception scoped to css files' do _(Site.valid_file_mime_type_and_ext?('application/x-appleworks3', '.txt')).must_be_nil end end describe 'tags' do it 'should handle empty tags in comma-separated list' do site = Fabricate.build :site, new_tags_string: 'art, , Seancore, microlables' _(site.valid?).must_equal true site.save _(site.tags.map(&:name).sort).must_equal ['art', 'microlables', 'seancore'] end it 'should handle multiple empty tags' do site = Fabricate.build :site, new_tags_string: ', art, , , test, ' _(site.valid?).must_equal true site.save _(site.tags.map(&:name).sort).must_equal ['art', 'test'] end end describe 'email validation' do it 'accepts valid email addresses' do valid_emails = [ 'user@example.com', 'test.email@domain.org', 'user+tag@example.com', 'user_name@domain.co.uk', 'user-name@example-domain.com', 'user@xn--1lqs71d.xn--wgv71a', 'a.very.long.email.address@long-domain-name.example.com' ] valid_emails.each do |email| site = Fabricate.build(:site, email: email) site.valid? email_errors = site.errors[:email] || [] format_errors = email_errors.select { |e| e.include?('valid email') } _(format_errors).must_be_empty end end it 'rejects invalid email addresses' do invalid_emails = [ 'user@domain.com💩💩💩', 'user@domain.com extra text', 'user @domain.com', 'user@domain', 'user@domain.', '@domain.com', 'user@', 'plaintext', ] invalid_emails.each do |email| site = Fabricate.build(:site, email: email) site.valid? email_errors = site.errors[:email] || [] format_errors = email_errors.select { |e| e.include?('valid email') } _(format_errors).wont_be_empty end end it 'rejects emails that exceed byte length limit' do long_email = 'user@example.com' + 'a' * 300 # Will exceed 254 byte limit site = Fabricate.build(:site, email: long_email) site.valid? email_errors = site.errors[:email] || [] length_errors = email_errors.select { |e| e.include?('too long') } _(length_errors).wont_be_empty end it 'rejects emails with unicode padding that exceed byte limit' do unicode_email = 'user@example.com' + '⠀' * 100 # Braille blank chars _(unicode_email.bytesize).must_be :>, Site::MAX_EMAIL_LENGTH site = Fabricate.build(:site, email: unicode_email) site.valid? email_errors = site.errors[:email] || [] length_errors = email_errors.select { |e| e.include?('too long') } _(length_errors).wont_be_empty end it 'accepts emails just under the byte limit' do under_limit_email = 'a' * (Site::MAX_EMAIL_LENGTH - 15) + '@example.com' _(under_limit_email.bytesize).must_be :<=, Site::MAX_EMAIL_LENGTH site = Fabricate.build(:site, email: under_limit_email) site.valid? email_errors = site.errors[:email] || [] length_errors = email_errors.select { |e| e.include?('too long') } _(length_errors).must_be_empty end end describe 'bitcoin address validation' do it 'accepts valid bitcoin addresses' do valid_addresses = [ '1BoatSLRHtKNngkdXEeobR76b53LETtpyT', '3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080', 'BC1TESTADDRESS' ] valid_addresses.each do |address| _(Site.valid_bitcoin_address?(address)).must_equal true end end it 'rejects invalid bitcoin addresses' do invalid_addresses = [ 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty', 'bcrt1q4d4v8g4qv9nwx7v6p5d3y8d6p8ygj5y0yl9q9', 'mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn', 'notabitcoinaddress' ] invalid_addresses.each do |address| _(Site.valid_bitcoin_address?(address)).must_equal false end end end describe 'banning' do it 'still makes files available' do site = Fabricate :site site.ban! _(File.exist?(site.current_files_path('index.html'))).must_equal true _(site.current_files_path('index.html')).must_equal File.join(Site::DELETED_SITES_ROOT, Site.sharding_dir(site.username), site.username, 'index.html') end it 'purges cache by default' do site = Fabricate :site PurgeCacheWorker.jobs.clear $redis_proxy.del(PurgeCacheWorker::PURGE_STREAM_KEY) expected_purges = site.site_files_dataset.count site.ban! PurgeCacheWorker.drain stream_length = $redis_proxy.xlen(PurgeCacheWorker::PURGE_STREAM_KEY) expected_stream_entries = expected_purges * (PurgeCacheWorker::FOLLOWUP_PURGE_DELAYS.length + 1) _(stream_length).must_equal expected_stream_entries end it 'can skip cache purge' do site = Fabricate :site PurgeCacheWorker.jobs.clear $redis_proxy.del(PurgeCacheWorker::PURGE_STREAM_KEY) site.ban!(false) PurgeCacheWorker.drain stream_length = $redis_proxy.xlen(PurgeCacheWorker::PURGE_STREAM_KEY) _(stream_length).must_equal 0 end end describe 'unban' do it 'works' do site = Fabricate :site index_path = File.join site.base_files_path, 'index.html' site.ban! _(File.exist?(index_path)).must_equal false site.unban! site.reload _(site.is_banned).must_equal false _(site.banned_at).must_be_nil _(site.blackbox_whitelisted).must_equal true _(File.exist?(index_path)).must_equal true end end describe 'directory create' do it 'handles wacky pathnames' do ['/derp', '/derp/'].each do |path| site = Fabricate :site site_file_count = site.site_files_dataset.count site.create_directory path _(site.site_files.select {|s| s.path == '' || s.path == '.'}.length).must_equal 0 _(site.site_files.select {|s| s.path == path.gsub('/', '')}.first).wont_be_nil _(site.site_files_dataset.count).must_equal site_file_count+1 end end it 'scrubs ../ from directory' do site = Fabricate :site site.create_directory '../../test' _(site.site_files.select {|site_file| site_file.path =~ /\.\./}.length).must_equal 0 end it 'blocks long directory create' do site = Fabricate :site long_path_string = 'a' * (SiteFile::FILE_PATH_CHARACTER_LIMIT + 1) res = site.create_directory long_path_string _(res).must_equal 'Directory path is too long.' end it 'blocks individual directory names that are too long' do site = Fabricate :site long_dir_name = 'a' * (SiteFile::FILE_NAME_CHARACTER_LIMIT + 1) res = site.create_directory "somedir/#{long_dir_name}/anotherdir" _(res).must_match /name is too long/i end it 'blocks directory names with backslashes' do site = Fabricate :site res = site.create_directory 'bad\\dir' _(res).must_equal 'Directory path contains invalid characters.' _(site.site_files_dataset.where(path: 'bad\\dir').count).must_equal 0 end end describe 'scrubbed_path' do it 'preserves literal percent characters without URL decoding' do site = Fabricate :site test_paths = [ '100% awesome.jpg', 'derpking/70%off.png', '50% sale.txt', 'discount%special.png', 'garfield is 100% sexy.jpg', 'path/with/100%valid.txt' ] test_paths.each do |path| scrubbed = site.scrubbed_path(path) _(scrubbed).must_equal path # Should be exactly the same - no URL decoding end end it 'still handles path traversal and other security issues' do site = Fabricate :site # Should still block path traversal _(site.scrubbed_path('../../../etc/passwd')).must_equal 'etc/passwd' _(site.scrubbed_path('../../test')).must_equal 'test' # Should still remove empty components and dots _(site.scrubbed_path('/./test/./file.txt')).must_equal 'test/file.txt' _(site.scrubbed_path('test//file.txt')).must_equal 'test/file.txt' # But percent characters should be preserved _(site.scrubbed_path('test/70%off.png')).must_equal 'test/70%off.png' end it 'raises error for control characters' do site = Fabricate :site # Should still raise error for control characters (below ASCII 32) _(proc { site.scrubbed_path("test\x00file.txt") }).must_raise ArgumentError _(proc { site.scrubbed_path("test\x1Ffile.txt") }).must_raise ArgumentError end end describe 'custom_max_space' do it 'should use the custom max space if it is more' do site = Fabricate :site _(site.maximum_space).must_equal Site::PLAN_FEATURES[:free][:space] site.custom_max_space = 10**9 site.save_changes _(site.maximum_space).must_equal 10**9 end end describe 'can_email' do it 'should fail if send_emails is false' do site = Fabricate :site _(site.can_email?).must_equal true site.update send_emails: false _(site.can_email?).must_equal false _(site.can_email?(:send_comment_emails)).must_equal false site.update send_emails: true _(site.can_email?(:send_comment_emails)).must_equal true site.update send_comment_emails: false _(site.can_email?(:send_comment_emails)).must_equal false end end describe 'title extraction from HTML' do before do @site = Fabricate :site end it 'extracts valid UTF-8 titles correctly' do valid_html = '