diff --git a/app.rb b/app.rb index 0dc4422a..52accc05 100644 --- a/app.rb +++ b/app.rb @@ -77,6 +77,12 @@ before do end end +after do + if @api + request.session_options[:skip] = true + end +end + #after do #response.headers['Content-Security-Policy'] = %{block-all-mixed-content; default-src 'self'; connect-src 'self' https://api.stripe.com; frame-src https://www.google.com/recaptcha/ https://js.stripe.com; script-src 'self' 'unsafe-inline' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: } #end diff --git a/app/api.rb b/app/api.rb index 02352ad9..60884132 100644 --- a/app/api.rb +++ b/app/api.rb @@ -5,6 +5,16 @@ get '/api' do erb :'api' end +post '/api/upload_hash' do + require_api_credentials + res = {} + files = [] + params.each do |k,v| + res[k] = current_site.sha1_hash_match? k, v + end + api_success files: res +end + get '/api/list' do require_api_credentials @@ -85,7 +95,7 @@ post '/api/delete' do api_error 400, 'missing_files', "#{path} was not found on your site, canceled deleting" end - if path == 'index.html' + if path == 'index.html' || path == '/index.html' api_error 400, 'cannot_delete_index', 'you cannot delete your index.html file, canceled deleting' end @@ -110,6 +120,12 @@ get '/api/info' do end end +get '/api/key' do + require_api_credentials + current_site.generate_api_key! if current_site.api_key.blank? + api_success api_key: current_site.api_key +end + def api_info_for(site) { info: { @@ -148,24 +164,31 @@ def init_api_credentials auth = request.env['HTTP_AUTHORIZATION'] begin - user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':') + if bearer_match = auth.match(/^Bearer (.+)/) + api_key = bearer_match.captures.first + api_error_invalid_auth if api_key.nil? || api_key.empty? + else + user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':') + end rescue api_error_invalid_auth end - if Site.valid_login? user, pass - site = Site[username: user] - - if site.nil? || site.is_banned - api_error_invalid_auth - end - - DB['update sites set api_calls=api_calls+1 where id=?', site.id].first - - session[:id] = site.id + if defined?(api_key) && !api_key.blank? + site = Site[api_key: api_key] + elsif defined?(user) && defined?(pass) + site = Site.get_site_from_login user, pass else api_error_invalid_auth end + + if site.nil? || site.is_banned || site.is_deleted + api_error_invalid_auth + end + + DB['update sites set api_calls=api_calls+1 where id=?', site.id].first + + session[:id] = site.id end def api_success(message_or_obj) @@ -189,7 +212,7 @@ def api_error(status, error_type, message) end def api_error_invalid_auth - api_error 403, 'invalid_auth', 'invalid credentials - please check your username and password' + api_error 403, 'invalid_auth', 'invalid credentials - please check your username and password (or your api key)' end def api_not_found diff --git a/app/password_reset.rb b/app/password_reset.rb index 2328c356..81383a9e 100644 --- a/app/password_reset.rb +++ b/app/password_reset.rb @@ -21,7 +21,7 @@ post '/send_password_reset' do body = <<-EOT Hello! This is the Neocities cat, and I have received a password reset request for your e-mail address. -Go to this URL to reset your password: http://neocities.org/password_reset_confirm?username=#{Rack::Utils.escape(site.username)}&token=#{token} +Go to this URL to reset your password: https://neocities.org/password_reset_confirm?username=#{Rack::Utils.escape(site.username)}&token=#{token} If you didn't request this password reset, you can ignore it. Or hide under a bed. Or take a nap. Your call. diff --git a/app/settings.rb b/app/settings.rb index f05d8e98..e8599730 100644 --- a/app/settings.rb +++ b/app/settings.rb @@ -168,6 +168,17 @@ post '/settings/:username/custom_domain' do end end +post '/settings/:username/generate_api_key' do + require_login + require_ownership_for_settings + is_new = current_site.api_key.nil? + current_site.generate_api_key! + + msg = is_new ? "New API key has been generated." : "API key has been regenerated." + flash[:success] = msg + redirect "/settings/#{current_site.username}#api_key" +end + post '/settings/change_password' do require_login diff --git a/config.yml.template b/config.yml.template index 885c797c..a55782d3 100644 --- a/config.yml.template +++ b/config.yml.template @@ -50,3 +50,4 @@ test: - mrteacher stop_forum_spam_api_key: testkey screenshots_url: http://screenshots:derp@screenshotssite.com + cache_control_ip: 1.2.3.4 diff --git a/config.yml.travis b/config.yml.travis index 3093ac19..ae274f3a 100644 --- a/config.yml.travis +++ b/config.yml.travis @@ -20,3 +20,4 @@ education_tag_whitelist: - mrteacher stop_forum_spam_api_key: testkey screenshots_url: http://screenshots:derp@screenshotssite.com +cache_control_ip: 1.2.3.4 diff --git a/migrations/103_sites_api_key.rb b/migrations/103_sites_api_key.rb new file mode 100644 index 00000000..b0cf0c64 --- /dev/null +++ b/migrations/103_sites_api_key.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + DB.add_column :sites, :api_key, :text + } + + down { + DB.drop_column :sites, :api_key + } +end diff --git a/models/site.rb b/models/site.rb index 87e0687b..5ba07c4a 100644 --- a/models/site.rb +++ b/models/site.rb @@ -70,6 +70,7 @@ class Site < Sequel::Model THUMBNAIL_RESOLUTIONS = ['210x158'] MAX_FILE_SIZE = 10**8 # 100 MB + MAX_SITE_DOWNLOAD_SIZE = 2_000_000_000 # 2GB CLAMAV_THREAT_MATCHES = [ /^VBS/, @@ -288,11 +289,8 @@ class Site < Sequel::Model def get_site_from_login(username_or_email, plaintext) site = get_with_identifier username_or_email - - return false if site.nil? - return false if site.is_deleted - return false if site.is_banned - site.valid_password?(plaintext) ? site : nil + return nil if site.nil? || site.is_deleted || site.is_banned || !site.valid_password?(plaintext) + site end def bcrypt_cost @@ -1138,6 +1136,10 @@ class Site < Sequel::Model ((total_space_used.to_f / maximum_space) * 100).round(1) end + def too_big_to_download? + space_used > MAX_SITE_DOWNLOAD_SIZE + end + # Note: Change Stat#prune! and the nginx map compiler if you change this business logic. def supporter? owner.plan_type != 'free' @@ -1499,6 +1501,17 @@ class Site < Sequel::Model true end + def generate_api_key! + self.api_key = SecureRandom.hex(16) + save_changes validate: false + end + + def sha1_hash_match?(path, sha1_hash) + relative_path = scrubbed_path path + site_file = site_files_dataset.where(path: relative_path, sha1_hash: sha1_hash).first + !site_file.nil? + end + private def store_file(path, uploaded, opts={}) diff --git a/models/stat.rb b/models/stat.rb index fd5e5992..7f8ef87f 100644 --- a/models/stat.rb +++ b/models/stat.rb @@ -19,7 +19,7 @@ class Stat < Sequel::Model def parse_logfiles(path) total_site_stats = {} - cache_control_ip = Resolv::DNS.new.getaddress('neocities.org') + cache_control_ip = $config['cache_control_ip'] Dir["#{path}/*.log"].each do |log_path| site_logs = {} @@ -276,4 +276,3 @@ end end end =end - diff --git a/tests/api_tests.rb b/tests/api_tests.rb index 3f1669ea..e7fdc281 100644 --- a/tests/api_tests.rb +++ b/tests/api_tests.rb @@ -197,6 +197,50 @@ describe 'api delete' do end end +describe 'api key' do + it 'generates new key with valid login' do + create_site + basic_authorize @user, @pass + get '/api/key' + res[:result].must_equal 'success' + res[:api_key].must_equal @site.reload.api_key + end + + it 'returns existing key' do + create_site + @site.generate_api_key! + basic_authorize @user, @pass + get '/api/key' + res[:api_key].must_equal @site.api_key + end + + it 'fails for bad login' do + create_site + basic_authorize 'zero', 'cool' + get '/api/key' + res[:error_type].must_equal 'invalid_auth' + end +end + +describe 'api upload hash' do + it 'succeeds' do + create_site + basic_authorize @user, @pass + test_hash = Digest::SHA1.file('./tests/files/test.jpg').hexdigest + + post '/api/upload', { + 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg'), + 'test2.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + } + + post '/api/upload_hash', "test.jpg" => test_hash, "test2.jpg" => Digest::SHA1.hexdigest('herpderp') + + res[:result].must_equal 'success' + res[:files][:'test.jpg'].must_equal true + res[:files][:'test2.jpg'].must_equal false + end +end + describe 'api upload' do it 'fails with no auth' do post '/api/upload' @@ -217,6 +261,26 @@ describe 'api upload' do res[:error_type].must_equal 'missing_files' end + it 'succeeds with valid api key' do + create_site + @site.api_key.must_equal nil + @site.generate_api_key! + @site.reload.api_key.wont_equal nil + header 'Authorization', "Bearer #{@site.api_key}" + post '/api/upload', 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + res[:result].must_equal 'success' + site_file_exists?('test.jpg').must_equal true + end + + it 'fails with bad api key' do + create_site + @site.generate_api_key! + header 'Authorization', "Bearer zerocool" + post '/api/upload', 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg') + res[:result].must_equal 'error' + res[:error_type].must_equal 'invalid_auth' + end + =begin # Getting too slow to run this test it 'fails with too many files' do diff --git a/views/_footer.erb b/views/_footer.erb index 8b5cfdaa..10423df3 100644 --- a/views/_footer.erb +++ b/views/_footer.erb @@ -3,7 +3,7 @@

- Neocities is open source. Follow us on Twitter or Facebook + Neocities is open source. Follow us on Twitter or Facebook

@@ -190,9 +190,11 @@ <% if !current_site.plan_feature(:no_file_restrictions) %> Allowed file types | <% end %> - Download entire site + <% unless current_site.too_big_to_download? %> + Download entire site | + <% end %> <% unless is_education? %> - | Mount your site as a drive on your computer! + Mount your site as a drive on your computer <% end %> diff --git a/views/permanent_web.erb b/views/permanent_web.erb index f44f47bb..a1e027be 100644 --- a/views/permanent_web.erb +++ b/views/permanent_web.erb @@ -9,7 +9,7 @@

- Neocities has launched an experimental implementation of IPFS. IPFS is short for the "Inter-Planetary File System", and is the foundation for a new way to distribute web content that is being called The Permanent Web. The idea behind the Permanent Web is simple: Instead of serving web sites from central servers, we believe that web serving should be decentralized, and that IPFS is an eventual replacement to the aging HTTP protocol for serving static web sites. + Neocities has launched an experimental implementation of IPFS. IPFS is short for the "Inter-Planetary File System", and is the foundation for a new way to distribute web content that is being called The Permanent Web. The idea behind the Permanent Web is simple: Instead of serving web sites from central servers, we believe that web serving should be decentralized, and that IPFS is an eventual replacement to the aging HTTP protocol for serving static web sites.

@@ -21,7 +21,7 @@

- If you want to play around with this new technology, you can get IPFS for your computer and use it to retrieve content from our IPFS node servers. All you need to do is download the IPFS daemon (OSX/Linux only for now), and run the following command in your terminal: + If you want to play around with this new technology, you can get IPFS for your computer and use it to retrieve content from our IPFS node servers. All you need to do is download the IPFS daemon (OSX/Linux only for now), and run the following command in your terminal:

diff --git a/views/settings/site.erb b/views/settings/site.erb index d1614157..5885bc71 100644 --- a/views/settings/site.erb +++ b/views/settings/site.erb @@ -29,6 +29,8 @@

  • Change Site (User) Name
  • Tipping
  • +
  • API Key
  • + <% if @site.admin_nsfw != true %>
  • 18+
  • <% end %> @@ -53,7 +55,9 @@
    <%== erb :'settings/site/tipping' %>
    - +
    + <%== erb :'settings/site/api_key' %> +
    <% if @site.admin_nsfw != true %>
    <%== erb :'settings/site/nsfw' %> diff --git a/views/settings/site/_api_key_form.erb b/views/settings/site/_api_key_form.erb new file mode 100644 index 00000000..6da4b930 --- /dev/null +++ b/views/settings/site/_api_key_form.erb @@ -0,0 +1,4 @@ +
    + <%== csrf_token_input_html %> + +
    diff --git a/views/settings/site/api_key.erb b/views/settings/site/api_key.erb new file mode 100644 index 00000000..769803c6 --- /dev/null +++ b/views/settings/site/api_key.erb @@ -0,0 +1,19 @@ +

    API Key

    + +

    + An API Key can be used with the Neocities API to allow for changes to your site without requiring login credentials. This API key allows full write access to your site. Keep it secret, keep it safe. +

    + +<% if @site.api_key %> +

    Your API key:
    <%= @site.api_key %>

    + +

    + If you need to regenerate the key for some reason, you can do that below. + Keep in mind that this will make the old key no longer function. +

    + + <%== erb :'settings/site/_api_key_form', layout: false %> +<% else %> +

    You haven't yet generated an API key, click the button to create one:

    + <%== erb :'settings/site/_api_key_form', layout: false %> +<% end %> diff --git a/views/site_files/mount_info.erb b/views/site_files/mount_info.erb index c3015a9e..4b9e6f20 100644 --- a/views/site_files/mount_info.erb +++ b/views/site_files/mount_info.erb @@ -1,7 +1,7 @@

    Mount your site

    -

    Now you can access your Neocities site as a drive on your computer!

    +

    Access your Neocities site as a drive on your computer

    @@ -10,7 +10,7 @@

    About

    - Neocities now supports WebDAV, which allows you to mount your Neocities share on your computer. Now you can access and change your files on your own computer's file manager! + Neocities supports WebDAV, which allows you to mount your Neocities share on your computer.

    <% if current_site.nil? %> @@ -22,20 +22,24 @@ This feature requires a supporter account. Click here to become a supporter.

    <% else %> -

    Instructions for Windows 7

    +

    Instructions for Windows

    - Use these instructions, except use this URL to connect: https://neocities.org/webdav -
    - Enter your login info when requested. -

    + Unfortunately, the WebDAV that comes with Windows file manager has issues with SSL, + and the problem has not yet been fixed. We recommend using a client like Cyberduck with Windows, which is free and has + WebDAV support. You can also try Mountain Duck + which will allow you to mount your Neocities site as a hard drive on your computer.

    Instructions for OSX

    - Use these instructions, and use this URL to connect: https://neocities.org/webdav -
    - Enter login info when requested. + Use these instructions, and use this URL to connect: https://neocities.org/webdav +
    + Enter login info when requested. +

    + +

    + You can also try out Cyberduck or Mountain Duck if you run into any issues.

    Instructions for Linux (Ubuntu)

    diff --git a/views/templates/index.erb b/views/templates/index.erb index 4324ab61..a2654594 100644 --- a/views/templates/index.erb +++ b/views/templates/index.erb @@ -11,7 +11,7 @@

    Welcome to my Website!

    -

    This is a paragraph! Here's how you make a link: Neocities.

    +

    This is a paragraph! Here's how you make a link: Neocities.

    Here's how you can make bold and italic text.

    @@ -26,6 +26,6 @@
  • Third thing
  • -

    To learn more HTML/CSS, check out these tutorials!

    +

    To learn more HTML/CSS, check out these tutorials!