-
Notifications
You must be signed in to change notification settings - Fork 548
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Convert library to use built-in
Net::HTTP
Moves the library off of Faraday and over onto the standard library's built-in `Net::HTTP` module. The upside of the transition is that we break away from a few dependencies that have caused us a fair bit of trouble in the past, the downside is that we need more of our own code to do things (although surprisingly, not that much more). The biggest new pieces are: * `ConnectionManager`: A per-thread class that manages a connection to each Stripe infrastructure URL (like `api.stripe.com`, `connect.stripe.com`, etc.) so that we can reuse them between requests. It's also responsible for setting up and configuring new `Net::HTTP` connections, which is a little more heavyweight code-wise compared to other libraries. All of this could have lived in `StripeClient`, but I extracted it because that class has gotten so big. * `MultipartEncoder`: A class that does multipart form encoding for file uploads. Unfortunately, Ruby doesn't bundle anything like this. I built this by referencing the Go implementation because the original RFC is not very detailed or well-written. I also made sure that it was behaving similarly to our other custom implementations like stripe-node, and that it can really upload a file outside the test suite. There's some risk here in that it's easy to miss something across one of these big transitions. I've tried to test out various error cases through tests, but also by leaving scripts running as I terminate my network connection and bring it back. That said, we'd certainly release on a major version bump because some of the interface (like setting `Stripe.default_client`) changes.
- Loading branch information
Showing
18 changed files
with
850 additions
and
272 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
# frozen_string_literal: true | ||
|
||
module Stripe | ||
# Manages connections across multiple hosts which is useful because the | ||
# library may connect to multiple hosts during a typical session (main API, | ||
# Connect, Uploads). Ruby doesn't provide an easy way to make this happen | ||
# easily, so this class is designed to track what we're connected to and | ||
# manage the lifecycle of those connections. | ||
# | ||
# Note that this class in itself is *not* thread safe. We expect it to be | ||
# instantiated once per thread. | ||
# | ||
# Note also that this class doesn't currently clean up after itself because | ||
# it expects to only ever have a few connections. It'd be possible to tank | ||
# memory by constantly changing the value of `Stripe.api_base` or the like. A | ||
# possible improvement might be to detect and prune old connections whenever | ||
# a request is executed. | ||
class ConnectionManager | ||
def initialize | ||
@active_connections = {} | ||
end | ||
|
||
# Gets a connection for a given URI. This is for internal use only as it's | ||
# subject to change (we've moved between HTTP client schemes in the past | ||
# and may do it again). | ||
# | ||
# `uri` is expected to be a string. | ||
def connection_for(uri) | ||
u = URI.parse(uri) | ||
connection = @active_connections[[u.host, u.port]] | ||
|
||
if connection.nil? | ||
connection = create_connection(u) | ||
|
||
# TODO: what happens after TTL? | ||
connection.start | ||
|
||
@active_connections[[u.host, u.port]] = connection | ||
end | ||
|
||
connection | ||
end | ||
|
||
# Executes an HTTP request to the given URI with the given method. Also | ||
# allows a request body, headers, and query string to be specified. | ||
def execute_request(method, uri, body: nil, headers: nil, query: nil) | ||
# Perform some basic argument validation because it's easy to get | ||
# confused between strings and hashes for things like body and query | ||
# parameters. | ||
raise ArgumentError, "method should be a symbol" \ | ||
unless method.is_a?(Symbol) | ||
raise ArgumentError, "uri should be a string" \ | ||
unless uri.is_a?(String) | ||
raise ArgumentError, "body should be a string" \ | ||
if body && !body.is_a?(String) | ||
raise ArgumentError, "headers should be a hash" \ | ||
if headers && !headers.is_a?(Hash) | ||
raise ArgumentError, "query should be a string" \ | ||
if query && !query.is_a?(String) | ||
|
||
connection = connection_for(uri) | ||
|
||
u = URI.parse(uri) | ||
path = if query | ||
u.path + "?" + query | ||
else | ||
u.path | ||
end | ||
|
||
connection.send_request(method.to_s.upcase, path, body, headers) | ||
end | ||
|
||
# | ||
# private | ||
# | ||
|
||
# `uri` should be a parsed `URI` object. | ||
private def create_connection(uri) | ||
# These all come back as `nil` if no proxy is configured. | ||
proxy_host, proxy_port, proxy_user, proxy_pass = proxy_parts | ||
|
||
connection = Net::HTTP.new(uri.host, uri.port, | ||
proxy_host, proxy_port, | ||
proxy_user, proxy_pass) | ||
|
||
connection.open_timeout = Stripe.open_timeout | ||
connection.read_timeout = Stripe.read_timeout | ||
|
||
connection.use_ssl = uri.scheme == "https" | ||
|
||
if Stripe.verify_ssl_certs | ||
connection.verify_mode = OpenSSL::SSL::VERIFY_PEER | ||
connection.cert_store = Stripe.ca_store | ||
else | ||
connection.verify_mode = OpenSSL::SSL::VERIFY_NONE | ||
|
||
unless @verify_ssl_warned | ||
@verify_ssl_warned = true | ||
warn("WARNING: Running without SSL cert verification. " \ | ||
"You should never do this in production. " \ | ||
"Execute `Stripe.verify_ssl_certs = true` to enable " \ | ||
"verification.") | ||
end | ||
end | ||
|
||
connection | ||
end | ||
|
||
# `Net::HTTP` somewhat awkwardly requires each component of a proxy URI | ||
# (host, port, etc.) rather than the URI itself. This method simply parses | ||
# out those pieces to make passing them into a new connection a little less | ||
# ugly. | ||
private def proxy_parts | ||
if Stripe.proxy.nil? | ||
[nil, nil, nil, nil] | ||
else | ||
u = URI.parse(Stripe.proxy) | ||
[u.host, u.port, u.user, u.password] | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
# frozen_string_literal: true | ||
|
||
require "securerandom" | ||
|
||
module Stripe | ||
# Encodes parameters into a `multipart/form-data` payload as described by RFC | ||
# 2388: | ||
# | ||
# https://tools.ietf.org/html/rfc2388 | ||
# | ||
# This is most useful for transferring file-like objects. | ||
# | ||
# Parameters should be added with `#encode`. When ready, use `#body` to get | ||
# the encoded result and `#content_type` to get the value that should be | ||
# placed in the `Content-Type` header of a subsequent request (which includes | ||
# a boundary value). | ||
class MultipartEncoder | ||
MULTIPART_FORM_DATA = "multipart/form-data".freeze | ||
|
||
# A shortcut for encoding a single set of parameters and finalizing a | ||
# result. | ||
# | ||
# Returns an encoded body and the value that should be set in the content | ||
# type header of a subsequent request. | ||
def self.encode(params) | ||
encoder = MultipartEncoder.new | ||
encoder.encode(params) | ||
encoder.close | ||
[encoder.body, encoder.content_type] | ||
end | ||
|
||
# Gets the object's randomly generated boundary string. | ||
attr_reader :boundary | ||
|
||
# Initializes a new multipart encoder. | ||
def initialize | ||
# We seed this with an empty string so that it encoding defaults to UTF-8 | ||
# instead of ASCII. The empty string is UTF-8 and new string inherits the | ||
# encoding of the string it's seeded with. | ||
@body = String.new("") | ||
|
||
# Chose the same number of random bytes that Go uses in its standard | ||
# library implementation. Easily enough entropy to ensure that it won't | ||
# be present in a file we're sending. | ||
@boundary = SecureRandom.hex(30) | ||
|
||
@closed = false | ||
@first_field = true | ||
end | ||
|
||
# Gets the encoded body. `#close` must be called first. | ||
def body | ||
raise "object must be closed before getting body" unless @closed | ||
@body | ||
end | ||
|
||
# Finalizes the object by writing the final boundary. | ||
def close | ||
raise "object already closed" if @closed | ||
|
||
@body << "\r\n" | ||
@body << "--#{@boundary}--" | ||
|
||
@closed = true | ||
|
||
nil | ||
end | ||
|
||
# Gets the value including boundary that should be put into a multipart | ||
# request's `Content-Type`. | ||
def content_type | ||
"#{MULTIPART_FORM_DATA}; boundary=#{@boundary}" | ||
end | ||
|
||
# Encodes a set of parameters to the body. | ||
# | ||
# Note that parameters are expected to be a hash, but a "flat" hash such | ||
# that complex substructures like hashes and arrays have already been | ||
# appropriately Stripe-encoded. Pass a complex structure through | ||
# `Util.flatten_params` first before handing it off to this method. | ||
def encode(params) | ||
raise "no more parameters can be written to closed object" if @closed | ||
|
||
params.each do |name, val| | ||
if val.is_a?(::File) | ||
val.rewind | ||
write_field(name, val.read, filename: ::File.basename(val.path)) | ||
elsif val.respond_to?(:read) | ||
write_field(name, val.read, filename: "blob") | ||
else | ||
write_field(name, val, filename: nil) | ||
end | ||
end | ||
|
||
nil | ||
end | ||
|
||
# | ||
# private | ||
# | ||
|
||
# Escapes double quotes so that the given value can be used in a | ||
# double-quoted string and replaces any linebreak characters with spaces. | ||
private def escape(str) | ||
str.gsub('"', "%22").tr("\n", " ").tr("\r", " ") | ||
end | ||
|
||
private def write_field(name, data, filename:) | ||
if !@first_field | ||
@body << "\r\n" | ||
else | ||
@first_field = false | ||
end | ||
|
||
@body << "--#{@boundary}\r\n" | ||
|
||
if filename | ||
@body << %(Content-Disposition: form-data) + | ||
%(; name="#{escape(name.to_s)}") + | ||
%(; filename="#{escape(filename)}"\r\n) | ||
@body << %(Content-Type: application/octet-stream\r\n) | ||
else | ||
@body << %(Content-Disposition: form-data) + | ||
%(; name="#{escape(name.to_s)}"\r\n) | ||
end | ||
|
||
@body << "\r\n" | ||
@body << data.to_s | ||
end | ||
end | ||
end |
Oops, something went wrong.