···592592593593A successful but zero-length response will be returned.
594594595595+### Adding an attachment
596596+597597+Send a `POST` request to `$baseURL/ciphers/(cipher UUID)/attachment`
598598+599599+It is a multipart/form-data post, with the file under the `data`-attribute the single posted entity.
600600+601601+602602+ POST $baseURL/ciphers/(cipher UUID)/attachment
603603+ Content-type: application/json
604604+ Authorization: Bearer $access_token
605605+ {
606606+ "data": {
607607+ "filename": "encrypted_filename"
608608+ "tempfile": blob
609609+ }
610610+ }
611611+612612+The JSON response will then be the complete cipher item, but now containing an entry for the new attachment:
613613+614614+ {
615615+ "FolderId"=>nil,
616616+ ...
617617+ "Data"=> ...,
618618+ "Attachments"=>
619619+ [
620620+ { "Id"=>"7xytytjp1hc2ijy3n5y5vbbnzcukmo8b",
621621+ "Url"=> "https://cdn.bitwarden.com/attachments/(cipher UUID)/7xytytjp1hc2ijy3n5y5vbbnzcukmo8b",
622622+ "FileName"=> "2.GOkRA8iZio1KxB+UkJpfcA==|/Mc8ACbPr9CRRQmNKPYHVg==|4BBQf8YTbPupap6qR97qMdn0NJ88GdTgDPIyBsQ46aA=",
623623+ "Size"=>"65",
624624+ "SizeName"=>"65 Bytes",
625625+ "Object"=>"attachment"
626626+ }
627627+ ],
628628+ ...,
629629+ "Object"=>"cipher"
630630+ }
631631+632632+### Deleting an attachment
633633+634634+Send an empty `DELETE` request to `$baseURL/ciphers/(cipher UUID)/attachment/(attachment id)`:
635635+636636+ DELETE $baseURL/ciphers/(cipher UUID)/attachment/(attachment id)
637637+ Authorization: Bearer (access_token)
638638+639639+A successful but zero-length response will be returned.
640640+641641+### Downloading an attachment
642642+643643+$cdn_url using the official server is https://cdn.bitwarden.com.
644644+645645+Send an unauthenticated `GET` request to `$cdn_url/attachments/(cipher UUID)/(attachment id)`:
646646+647647+ GET $cdn_url/attachments/(cipher UUID)/(attachment id)
648648+649649+The file will be sent as a response.
650650+595651### Folders
596652597653To create a folder, `POST` to `$baseURL/folders`:
+7-6
Rakefile
···11-require "rake/testtask"
22-31# rake db:create_migration NAME=...
42require "sinatra/activerecord/rake"
5366-Rake::TestTask.new do |t|
77- t.pattern = "spec/*_spec.rb"
88-end
99-104namespace :db do
115 task :load_config do
126 require "./lib/rubywarden.rb"
137 end
148end
99+1010+require 'rake/testtask'
1111+1212+Rake::TestTask.new do |t|
1313+ t.libs << "spec"
1414+ t.pattern = "spec/*_spec.rb"
1515+end
···1818require 'sinatra/namespace'
19192020require_relative 'helpers/request_helpers'
2121+require_relative 'helpers/attachment_helpers'
21222223require_relative 'routes/api'
2324require_relative 'routes/icons'
2425require_relative 'routes/identity'
2626+require_relative 'routes/attachments'
25272628module Rubywarden
2729 class App < Sinatra::Base
···3638 end
37393840 helpers Rubywarden::RequestHelpers
4141+ helpers Rubywarden::AttachmentHelpers
39424043 before do
4144 if request.content_type.to_s.match(/\Aapplication\/json(;|\z)/)
···5861 register Rubywarden::Routing::Api
5962 register Rubywarden::Routing::Icons
6063 register Rubywarden::Routing::Identity
6464+ register Rubywarden::Routing::Attachments
6165 end
6266end
+54
lib/attachment.rb
···11+#
22+# Copyright (c) 2017 joshua stein <jcs@jcs.org>
33+#
44+# Permission to use, copy, modify, and distribute this software for any
55+# purpose with or without fee is hereby granted, provided that the above
66+# copyright notice and this permission notice appear in all copies.
77+#
88+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
99+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1010+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1111+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1212+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1313+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1414+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1515+#
1616+1717+class Attachment < DBModel
1818+ self.table_name = "attachments"
1919+ attr_accessor :context
2020+2121+ before_create :generate_uuid_primary_key
2222+ before_create :generate_url
2323+2424+ belongs_to :cipher, foreign_key: :cipher_uuid, inverse_of: :attachments
2525+2626+ def self.build_from_params(params, context)
2727+ attachment = new filename: params[:filename],
2828+ size: params[:size],
2929+ file: params[:file]
3030+ attachment.context = context
3131+ attachment
3232+ end
3333+3434+ def to_hash
3535+ {
3636+ "Id" => self.uuid,
3737+ "Url" => self.url,
3838+ "FileName" => self.filename.to_s,
3939+ "Size" => self.size,
4040+ "SizeName" => human_file_size,
4141+ "Object" => "attachment"
4242+ }
4343+ end
4444+4545+ private
4646+4747+ def generate_url
4848+ self.url = context.url("/attachments/#{self.cipher_uuid}/#{self.id}")
4949+ end
5050+5151+ def human_file_size
5252+ ActiveSupport::NumberHelper.number_to_human_size(self.size)
5353+ end
5454+end
···11+#
22+# Copyright (c) 2018 joshua stein <jcs@jcs.org>
33+#
44+# Permission to use, copy, modify, and distribute this software for any
55+# purpose with or without fee is hereby granted, provided that the above
66+# copyright notice and this permission notice appear in all copies.
77+#
88+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
99+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1010+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1111+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1212+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1313+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1414+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1515+#
1616+1717+module Rubywarden
1818+ module AttachmentHelpers
1919+ def retrieve_cipher(uuid: )
2020+ d = device_from_bearer
2121+ if !d
2222+ halt validation_error("invalid bearer")
2323+ end
2424+2525+ c = nil
2626+ if uuid.blank? || !(c = Cipher.find_by_user_uuid_and_uuid(d.user_uuid, uuid))
2727+ halt validation_error("invalid cipher")
2828+ end
2929+ return c
3030+ end
3131+3232+ def delete_attachment uuid:, attachment_uuid:
3333+ cipher = retrieve_cipher uuid: uuid
3434+ cipher.attachments.find(attachment_uuid).destroy
3535+ ""
3636+ end
3737+ end
3838+end
+14
lib/helpers/request_helpers.rb
···4545 "Object" => "error",
4646 }.to_json ]
4747 end
4848+4949+ def delete_cipher app:, uuid:
5050+ d = device_from_bearer
5151+ if !d
5252+ halt validation_error("invalid bearer")
5353+ end
5454+5555+ c = nil
5656+ if uuid.blank? || !(c = Cipher.find_by_user_uuid_and_uuid(d.user_uuid, uuid))
5757+ halt validation_error("invalid cipher")
5858+ end
5959+ c.destroy
6060+ ""
6161+ end # delete_cipher
4862 end
4963end
+1-14
lib/routes/api.rb
···209209210210 # delete a cipher
211211 delete "/ciphers/:uuid" do
212212- d = device_from_bearer
213213- if !d
214214- return validation_error("invalid bearer")
215215- end
216216-217217- c = nil
218218- if params[:uuid].blank? ||
219219- !(c = Cipher.find_by_user_uuid_and_uuid(d.user_uuid, params[:uuid]))
220220- return validation_error("invalid cipher")
221221- end
222222-223223- c.destroy
224224-225225- ""
212212+ delete_cipher app: app, uuid: params[:uuid]
226213 end
227214228215 #
+69
lib/routes/attachments.rb
···11+#
22+# Copyright (c) 2018 joshua stein <jcs@jcs.org>
33+#
44+# Permission to use, copy, modify, and distribute this software for any
55+# purpose with or without fee is hereby granted, provided that the above
66+# copyright notice and this permission notice appear in all copies.
77+#
88+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
99+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1010+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1111+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1212+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1313+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1414+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1515+#
1616+# uses helpers/attachment_helpers
1717+module Rubywarden
1818+ module Routing
1919+ module Attachments
2020+ def self.registered(app)
2121+ app.namespace BASE_URL do
2222+ post "/ciphers/:uuid/attachment" do
2323+ cipher = retrieve_cipher uuid: params[:uuid]
2424+2525+ need_params(:data) do |p|
2626+ return validation_error("#{p} cannot be blank")
2727+ end
2828+2929+ # we have to extract filename from data -> head, since data -> filename is truncated
3030+ filename = nil
3131+ if md = params[:data][:head].match(/filename=\"(\S+)\"\r\nContent-Type/)
3232+ filename = md[1]
3333+ else
3434+ return validation_error("filename cannot be blank")
3535+ end
3636+3737+ file = params[:data][:tempfile]
3838+ attachment_params = { filename: filename,
3939+ size: file.size,
4040+ file: file.read }
4141+ attachment = cipher.attachments.build_from_params(attachment_params, self)
4242+4343+ Attachment.transaction do
4444+ if !attachment.save
4545+ return validation_error("error saving")
4646+ end
4747+4848+ cipher.to_hash.to_json
4949+ end
5050+ end
5151+5252+ delete "/ciphers/:uuid/attachment/:attachment_id" do
5353+ delete_attachment uuid: params[:uuid], attachment_uuid: params[:attachment_id]
5454+ end
5555+5656+ post "/ciphers/:uuid/attachment/:attachment_id/delete" do
5757+ delete_attachment uuid: params[:uuid], attachment_uuid: params[:attachment_id]
5858+ end
5959+ end # BASE_URL
6060+6161+ app.get "/attachments/:uuid/:attachment_id" do
6262+ a = Attachment.find_by_uuid_and_cipher_uuid(params[:attachment_id], params[:uuid])
6363+ attachment(a.filename)
6464+ response.write(a.file)
6565+ end
6666+ end # registered app
6767+ end
6868+ end
6969+end
···33require "minitest/autorun"
44require "rack/test"
5566-# clear out test db
77-if File.exist?(f = File.dirname(__FILE__) + "/../db/test.sqlite3")
88- File.unlink(f)
99-end
1010-116# most tests require this to be on
127ALLOW_SIGNUPS = true
138