An unofficial, mostly Bitwarden-compatible API server written in Ruby (Sinatra and ActiveRecord)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 5ccd4174b00a4af77c2baf9d7ab6a45eced19156 151 lines 4.1 kB view raw
1#!/usr/bin/env ruby 2# 3# Copyright (c) 2017 joshua stein <jcs@jcs.org> 4# 5# Permission to use, copy, modify, and distribute this software for any 6# purpose with or without fee is hereby granted, provided that the above 7# copyright notice and this permission notice appear in all copies. 8# 9# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16# 17 18# 19# A simple proxy intercepting API calls from a Bitwarden client, dumping them 20# out, sending them off to the real Bitwarden servers, dumping the response, 21# and sending it back to the client 22# 23 24require "sinatra" 25require "cgi" 26require "net/https" 27 28set :bind, "0.0.0.0" 29 30# log full queries, otherwise just pretty-printed request and response data 31RAW_QUERIES = false 32 33BASE_URL = "/api" 34IDENTITY_BASE_URL = "/identity" 35ICONS_URL = "/icons" 36 37def upstream_url_for(url) 38 if url.match(/^#{Regexp.escape(IDENTITY_BASE_URL)}/) 39 "https://identity.bitwarden.com" + url.gsub(/^#{Regexp.escape(IDENTITY_BASE_URL)}/, "") 40 elsif url.match(/^#{Regexp.escape(ICONS_URL)}/) 41 "https://icons.bitwarden.com" + url.gsub(/^#{Regexp.escape(ICONS_URL)}/, "") 42 else 43 "https://api.bitwarden.com" + url.gsub(/^#{Regexp.escape(BASE_URL)}/, "") 44 end 45end 46 47# hack in a way to get the actual-cased headers 48module Net::HTTPHeader 49 alias_method :old_add_field, :add_field 50 51 def actual_headers 52 @actual_headers 53 end 54 55 def add_field(key, val) 56 @actual_headers ||= {} 57 @actual_headers[key] = val 58 59 old_add_field key, val 60 end 61end 62 63delete /(.*)/ do 64 proxy_to upstream_url_for(request.path_info), :delete 65end 66 67get /(.*)/ do 68 proxy_to upstream_url_for(request.path_info), :get 69end 70 71post /(.*)/ do 72 proxy_to upstream_url_for(request.path_info), :post 73end 74 75put /(.*)/ do 76 proxy_to upstream_url_for(request.path_info), :put 77end 78 79def proxy_to(url, method) 80 puts "proxying #{method.to_s.upcase} to #{url}" 81 82 uri = URI.parse(url) 83 h = Net::HTTP.new(uri.host, uri.port) 84 if RAW_QUERIES 85 h.set_debug_output STDOUT 86 end 87 88 if uri.scheme == "https" 89 h.use_ssl = true 90 end 91 92 send_headers = { 93 "Content-type" => (request.env["CONTENT_TYPE"] || "application/x-www-form-urlencoded"), 94 "Host" => uri.host, 95 "User-Agent" => request.env["HTTP_USER_AGENT"], 96 # disable gzip to make it easier to inspect 97 "Accept-Encoding" => "identity", 98 } 99 100 if a = request.env["HTTP_AUTHORIZATION"] 101 send_headers["Authorization"] = a 102 end 103 104 post_data = request.body.read.to_s 105 106 unless RAW_QUERIES 107 if send_headers["Content-type"].to_s.match(/\/json/i) 108 puts "client JSON request:", 109 JSON.pretty_generate(JSON.parse(post_data)) 110 else 111 puts "client request: #{post_data.inspect}" 112 end 113 end 114 115 res = case method 116 when :post 117 res = h.post(uri.path, post_data, send_headers) 118 when :get 119 res = h.get(uri.path, send_headers) 120 when :put 121 res = h.put(uri.path, post_data, send_headers) 122 when :delete 123 res = h.delete(uri.path, send_headers) 124 else 125 raise "unknown method type #{method.inspect}" 126 end 127 128 reply_headers = res.actual_headers.reject{|k,v| 129 [ "Connection", "Transfer-Encoding" ].include?(k) 130 } 131 132 r = [ res.code.to_i, reply_headers, res.body ] 133 134 unless RAW_QUERIES 135 if reply_headers["Content-Type"].to_s.match(/\/json/) 136 begin 137 puts "proxy JSON reponse:", 138 JSON.pretty_generate(JSON.parse(res.body)) 139 rescue JSON::ParserError => e 140 puts "failed parsing JSON response: #{e.message}" 141 puts "proxy response: #{res.body}" 142 end 143 elsif reply_headers["Content-Type"].to_s.match(/image\//i) 144 puts "(image data of size #{res.body.bytesize} returned)" 145 else 146 puts "proxy response: #{res.body}" 147 end 148 end 149 150 r 151end