|
| 1 | +#!/usr/bin/env ruby -r rubygems |
| 2 | +# frozen_string_literal: true |
| 3 | + |
| 4 | +# Usage examples: |
| 5 | +# ./scheme_data_importer.rb \ |
| 6 | +# --script-id 8xx \ |
| 7 | +# --deploy-id x \ |
| 8 | +# --realm 7xxxxxxx \ |
| 9 | +# --consumer-key 4xxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ |
| 10 | +# --consumer-secret 2xxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ |
| 11 | +# --token 7xxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ |
| 12 | +# --token-secret 5xxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| 13 | +# |
| 14 | +# This example demonstrates how to sign an OAuth 1.0 HMAC-SHA256 GET request |
| 15 | +# including oauth params and query params in the signature base string, and |
| 16 | +# send the Authorization header to a NetSuite-hosted endpoint. |
| 17 | + |
| 18 | +require "oauth" |
| 19 | +require "net/http" |
| 20 | +require "uri" |
| 21 | +require "json" |
| 22 | +require "securerandom" |
| 23 | +require "openssl" |
| 24 | +require "base64" |
| 25 | +require "optparse" |
| 26 | +require "pp" |
| 27 | + |
| 28 | +class SchemeDataImporter |
| 29 | + attr_reader :script_id, :deploy_id, :realm, :consumer, :token |
| 30 | + |
| 31 | + def initialize(script_id, deploy_id, realm, consumer_key, consumer_secret, token_value, token_secret) |
| 32 | + @script_id = script_id |
| 33 | + @deploy_id = deploy_id |
| 34 | + @realm = realm |
| 35 | + @consumer = OAuth::Consumer.new(consumer_key, consumer_secret, { |
| 36 | + site: "https://#{realm}.xyz.netsuite.com", |
| 37 | + signature_method: "HMAC-SHA256", |
| 38 | + oauth_version: "1.0", |
| 39 | + }) |
| 40 | + |
| 41 | + # AccessToken must be constructed with the token and token_secret |
| 42 | + @token = OAuth::AccessToken.new(@consumer, token_value, token_secret) |
| 43 | + @token_secret = token_secret || "" |
| 44 | + end |
| 45 | + |
| 46 | + def import_data |
| 47 | + base_uri = "https://#{realm}.xyz.netsuite.com/app/site/hosting/xyz.nz" |
| 48 | + request_path = "#{base_uri}?script=#{OAuth::Helper.escape(script_id)}&deploy=#{OAuth::Helper.escape(deploy_id)}" |
| 49 | + |
| 50 | + auth_header, debug = generate_auth_header(base_uri, {"script" => script_id, "deploy" => deploy_id}) |
| 51 | + |
| 52 | + puts "Authorization header: #{auth_header}" |
| 53 | + puts "Base string: #{debug[:base_string]}" |
| 54 | + puts "Signing key: #{debug[:signing_key]}" |
| 55 | + puts "Signature: #{debug[:signature]}" |
| 56 | + |
| 57 | + uri = URI(request_path) |
| 58 | + http = Net::HTTP.new(uri.host, uri.port) |
| 59 | + http.use_ssl = (uri.scheme == "https") |
| 60 | + req = Net::HTTP::Get.new(uri.request_uri) |
| 61 | + req["Authorization"] = auth_header |
| 62 | + req["Content-Type"] = "application/json" |
| 63 | + req["Accept"] = "application/json" |
| 64 | + |
| 65 | + response = http.request(req) |
| 66 | + |
| 67 | + puts "Response Code: #{response.code}" |
| 68 | + puts "Response Message: #{response.message}" |
| 69 | + puts "Response Body: #{response.body}" |
| 70 | + |
| 71 | + if response.code.to_i == 200 |
| 72 | + puts "Import process complete" |
| 73 | + begin |
| 74 | + JSON.parse(response.body) |
| 75 | + rescue |
| 76 | + response.body |
| 77 | + end |
| 78 | + else |
| 79 | + raise "Failed to fetch data: #{response.code} #{response.message} - #{response.body}" |
| 80 | + end |
| 81 | + end |
| 82 | + |
| 83 | + private |
| 84 | + |
| 85 | + def generate_auth_header(base_uri, query_params = {}) |
| 86 | + oauth_params = { |
| 87 | + "oauth_consumer_key" => @consumer.key, |
| 88 | + "oauth_token" => @token.token, |
| 89 | + "oauth_signature_method" => "HMAC-SHA256", |
| 90 | + "oauth_timestamp" => Time.now.to_i.to_s, |
| 91 | + "oauth_nonce" => generate_nonce, |
| 92 | + "oauth_version" => "1.0", |
| 93 | + } |
| 94 | + |
| 95 | + all_params = oauth_params.merge(query_params) |
| 96 | + |
| 97 | + base_string = generate_base_string("GET", base_uri, all_params) |
| 98 | + |
| 99 | + signing_key = "#{OAuth::Helper.escape(@consumer.secret)}&#{OAuth::Helper.escape(@token_secret.to_s)}" |
| 100 | + |
| 101 | + digest = OpenSSL::HMAC.digest("sha256", signing_key, base_string) |
| 102 | + signature = Base64.strict_encode64(digest) |
| 103 | + |
| 104 | + oauth_params["oauth_signature"] = signature |
| 105 | + |
| 106 | + header_params = oauth_params.sort.map do |k, v| |
| 107 | + %(#{OAuth::Helper.escape(k)}="#{OAuth::Helper.escape(v)}") |
| 108 | + end |
| 109 | + header = "OAuth " + %(realm="#{OAuth::Helper.escape(realm)}") + ", " + header_params.join(", ") |
| 110 | + |
| 111 | + debug = { |
| 112 | + base_string: base_string, |
| 113 | + signing_key: signing_key, |
| 114 | + signature: signature, |
| 115 | + } |
| 116 | + |
| 117 | + [header, debug] |
| 118 | + end |
| 119 | + |
| 120 | + def generate_nonce |
| 121 | + SecureRandom.hex(16) |
| 122 | + end |
| 123 | + |
| 124 | + def generate_base_string(http_method, base_uri, params) |
| 125 | + encoded_pairs = params.map do |k, v| |
| 126 | + [OAuth::Helper.escape(k.to_s), OAuth::Helper.escape(v.to_s)] |
| 127 | + end |
| 128 | + |
| 129 | + encoded_pairs.sort_by! { |k, v| [k, v] } |
| 130 | + |
| 131 | + normalized = encoded_pairs.map { |k, v| "#{k}=#{v}" }.join("&") |
| 132 | + |
| 133 | + method = http_method.upcase |
| 134 | + "#{method}&#{OAuth::Helper.escape(base_uri)}&#{OAuth::Helper.escape(normalized)}" |
| 135 | + end |
| 136 | +end |
| 137 | + |
| 138 | +# ---------------- |
| 139 | +# CLI / Example runner |
| 140 | +options = {} |
| 141 | + |
| 142 | +op = OptionParser.new do |opts| |
| 143 | + opts.banner = "Usage: #{$PROGRAM_NAME} [options]" |
| 144 | + |
| 145 | + opts.on("--script-id ID", "Script ID to call") { |v| options[:script_id] = v } |
| 146 | + opts.on("--deploy-id ID", "Deploy ID to call") { |v| options[:deploy_id] = v } |
| 147 | + opts.on("--realm REALM", "Account realm (subdomain)") { |v| options[:realm] = v } |
| 148 | + opts.on("--consumer-key KEY", "Consumer key") { |v| options[:consumer_key] = v } |
| 149 | + opts.on("--consumer-secret SECRET", "Consumer secret") { |v| options[:consumer_secret] = v } |
| 150 | + opts.on("--token TOKEN", "Access token") { |v| options[:token] = v } |
| 151 | + opts.on("--token-secret SECRET", "Access token secret") { |v| options[:token_secret] = v } |
| 152 | +end |
| 153 | + |
| 154 | +op.parse! |
| 155 | + |
| 156 | +required = %i[script_id deploy_id realm consumer_key consumer_secret token token_secret] |
| 157 | +missing = required.select { |k| options[k].nil? } |
| 158 | +if missing.any? |
| 159 | + puts op.help |
| 160 | + puts "Missing options: #{missing.join(", ")}" |
| 161 | + exit 1 |
| 162 | +end |
| 163 | + |
| 164 | +importer = SchemeDataImporter.new( |
| 165 | + options[:script_id], |
| 166 | + options[:deploy_id], |
| 167 | + options[:realm], |
| 168 | + options[:consumer_key], |
| 169 | + options[:consumer_secret], |
| 170 | + options[:token], |
| 171 | + options[:token_secret], |
| 172 | +) |
| 173 | + |
| 174 | +begin |
| 175 | + result = importer.import_data |
| 176 | + pp(result) |
| 177 | +rescue => e |
| 178 | + warn("ERROR: #{e.message}") |
| 179 | + exit(1) |
| 180 | +end |
0 commit comments