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 @@
@@ -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 @@
+
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 @@
@@ -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!