Class: SmugMug::HTTP

Inherits:
Object
  • Object
show all
Defined in:
lib/smugmug/http.rb

Constant Summary collapse

API_URI =
URI("https://api.smugmug.com/services/api/json/1.3.0")
UPLOAD_URI =
URI("http://upload.smugmug.com/")
UPLOAD_HEADERS =
[:AlbumID, :Caption, :Altitude, :ImageID, :Keywords, :Latitude, :Longitude, :Hidden, :FileName]
OAUTH_ERRORS =
{30 => true, 32 => true, 33 => true, 35 => true, 36 => true, 37 => true, 38 => true, 98 => true}

Instance Method Summary collapse

Constructor Details

#initialize(args) ⇒ HTTP

Creates a new HTTP wrapper to handle the network portions of the API requests

Parameters:

  • args (Hash)

    Same as [SmugMug::HTTP]



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/smugmug/http.rb', line 17

def initialize(args)
  @config = args
  @digest = OpenSSL::Digest.new("SHA1")

  @headers = {"Accept-Encoding" => "gzip"}
  if args[:user_agent]
    @headers["User-Agent"] = "#{args.delete(:user_agent)} (ruby-smugmug v#{SmugMug::VERSION})"
  else
    @headers["User-Agent"] = "Ruby-SmugMug v#{SmugMug::VERSION}"
  end

  args[:http] = args.fetch(:http,{})  #checks for the existance of :http, sets to nill if does not exist.
  @http_proxy_host = args[:http].fetch(:proxy_host,nil)
  @http_proxy_port = args[:http].fetch(:proxy_port,nil)
  @http_proxy_user = args[:http].fetch(:proxy_user,nil)
  @http_proxy_pass = args[:http].fetch(:proxy_pass,nil)
end

Instance Method Details

#request(api, args) ⇒ Object



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/smugmug/http.rb', line 35

def request(api, args)
  uri = api == :uploading ? UPLOAD_URI : API_URI
  args[:method] = "smugmug.#{api}" unless api == :uploading

  http = ::Net::HTTP.new(uri.host, uri.port, @http_proxy_host, @http_proxy_port, @http_proxy_user, @http_proxy_pass)
  http.set_debug_output(@config[:debug_output]) if @config[:debug_output]

  # Configure HTTPS if needed
  if uri.scheme == "https"
    http.use_ssl = true

    if @config[:http] and @config[:http][:verify_mode]
      http.verify_mode = @config[:http][:verify_mode]
      http.ca_file = @config[:http][:ca_file]
      http.ca_path = @config[:http][:ca_path]
    else
      http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    end
  end

  # Upload request, which requires special handling
  if api == :uploading
    postdata = args.delete(:content)
    headers = @headers.merge("Content-Length" => postdata.length.to_s, "Content-MD5" => Digest::MD5.hexdigest(postdata), "X-Smug-Version" => "1.3.0", "X-Smug-ResponseType" => "JSON")

    UPLOAD_HEADERS.each do |key|
      next unless args[key] and args[key] != ""
      headers["X-Smug-#{key}"] = args[key].to_s
    end

    oauth = self.sign_request("POST", uri, nil)
    headers["Authorization"] = "OAuth oauth_consumer_key=\"#{oauth["oauth_consumer_key"]}\", oauth_nonce=\"#{oauth["oauth_nonce"]}\", oauth_signature_method=\"#{oauth["oauth_signature_method"]}\", oauth_signature=\"#{oauth["oauth_signature"]}\", oauth_timestamp=\"#{oauth["oauth_timestamp"]}\", oauth_version=\"#{oauth["oauth_version"]}\", oauth_token=\"#{oauth["oauth_token"]}\""

  # Normal API method
  else
    postdata = self.sign_request("POST", uri, args)
    headers = @headers
  end

  response = http.request_post(uri.request_uri, postdata, headers)
  if response.code == "204"
    return nil
  elsif response.code != "200"
    raise SmugMug::HTTPError.new("HTTP #{response.code}, #{response.message}", response.code, response.message)
  end

  # Check for GZIP encoding
  if response.header["content-encoding"] == "gzip"
    begin
      body = Zlib::GzipReader.new(StringIO.new(response.body)).read
    rescue Zlib::GzipFile::Error
      raise
    end
  else
    body = response.body
  end

  return nil if body == ""

  data = JSON.parse(body)

  if data["stat"] == "fail"
    # Special casing for SmugMug being in Read only mode
    if data["code"] == 99
      raise SmugMug::ReadonlyModeError.new("SmugMug is currently in read only mode, try again later")
    end

    klass = OAUTH_ERRORS[data["code"]] ? SmugMug::OAuthError : SmugMug::RequestError
    raise klass.new("Error ##{data["code"]}, #{data["message"]}", data["code"], data["message"])
  end

  data.delete("stat")
  data.delete("method")

  # smugmug.albums.changeSettings at the least doesn't return any data
  return nil if data.length == 0

  # It seems all smugmug APIs only return one hash of data, so this should be fine and not cause issues
  data.each do |_, value|
    return value
  end
end

#sign_request(method, uri, form_args) ⇒ Object

Generates an OAuth signature and updates the args with the required fields

Parameters:

  • method (String)

    HTTP method that the request is sent as

  • uri (String)

    Full URL of the request

  • form_args (Hash)

    Args to be passed to the server



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/smugmug/http.rb', line 123

def sign_request(method, uri, form_args)
  # Convert non-string keys to strings so the sort works
  args = {}
  if form_args
    form_args.each do |key, value|
      next unless value and value != ""

      key = key.to_s unless key.is_a?(String)
      args[key] = value
    end
  end

  # Add the necessary OAuth args
  args["oauth_version"] = "1.0"
  args["oauth_consumer_key"] = @config[:api_key]
  args["oauth_nonce"] = Digest::MD5.hexdigest("#{Time.now.to_f}#{rand(10 ** 30)}")
  args["oauth_signature_method"] = "HMAC-SHA1"
  args["oauth_timestamp"] = Time.now.utc.to_i
  args["oauth_token"] = @config[:user][:token]

  # RFC 1738 (http://www.ietf.org/rfc/rfc1738.txt) says:
  #
  #     Thus, only alphanumerics, the special characters "$-_.+!*'(),", and
  #     reserved characters used for their reserved purposes may be used
  #     unencoded within a URL.
  #
  # However, if we don't escape apostrophes and parentheses the SmugMug API fails
  # with an invalid signature error:
  #
  #     Error #35, invalid signature (SmugMug::OAuthError)
  #
  # To overcome this, define a new unreserved character list and use this in URI::escape
  unreserved = "\\-_.!~*a-zA-Z\\d"
  unsafe = Regexp.new("[^#{unreserved}]", false, 'N')

  # Sort the params
  sorted_args = []
  args.sort.each do |key, value|
    sorted_args.push("#{key.to_s}=#{URI::escape(value.to_s, unsafe)}")
  end

  postdata = sorted_args.join("&")

  # Final string to hash
  sig_base = "#{method}&#{URI::escape("#{uri.scheme}://#{uri.host}#{uri.path}", unsafe)}&#{URI::escape(postdata, unsafe)}"

  signature = OpenSSL::HMAC.digest(@digest, "#{@config[:oauth_secret]}&#{@config[:user][:secret]}", sig_base)
  signature = URI::escape(Base64.encode64(signature).chomp, unsafe)

  if uri == API_URI
    "#{postdata}&oauth_signature=#{signature}"
  else
    args["oauth_signature"] = signature
    args
  end
end