akamai_ccu 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c8ceea4c0a3db79103b414cf1ce2bbc11f8e3f23
4
+ data.tar.gz: 3f5f4bd41015eb19c8b52cede5c5b59a935e68be
5
+ SHA512:
6
+ metadata.gz: c4ea9b531d9555251a929ff94a9b47ac0119f6c7beccc792daf5ef9f63c5bfc3a5c8f879d18fc45913504fcca07c531d0ed6d7b990bfea06621de6647884b63a
7
+ data.tar.gz: 6c764ef12c97e32febdf45f6f0fc05e886d88624ad71a713445cb059a67f8056c8b6a3870dd6734954cf52910f07fe0013aaedf2e263dc1f8addfabe069ffb81
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.edgerc
11
+ /*.gem
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2.2
5
+ - 2.3.0
6
+ - 2.4.0
7
+ before_install: gem install bundler -v 1.15.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in akamai_ccu.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ ## Table of Contents
2
+
3
+ * [Scope](#scope)
4
+ * [Motivation](#motivation)
5
+ * [akamai-edgerid](#akamai-edgerid)
6
+ * [Installation](#installation)
7
+ * [Usage](#usage)
8
+ * [Configuration](#configuration)
9
+ * [edgerc](#edgerc)
10
+ * [txt](#txt)
11
+ * [Inside your script](#inside-your-script)
12
+ * [Secret](#secret)
13
+ * [Invalidating](#invalidating)
14
+ * [Deleting](#deleting)
15
+ * [Reuse client](#reuse-client)
16
+ * [CLI](#cli)
17
+ * [Help](#help)
18
+ * [invalidate](#invalidate)
19
+ * [delete](#delete)
20
+ * [Overwriting options](#overwriting-options)
21
+ * [Possible issues](#possible-issues)
22
+
23
+ ## Scope
24
+ This gem is a minimal wrapper of the [Akamai Content Control Utility](https://developer.akamai.com/api/purge/ccu/overview.html) APIs used to purge Edge content by request.
25
+ The library is compliant with [CCU API V3](https://developer.akamai.com/api/purge/ccu/resources.html), based on the *Fast Purge* utility.
26
+
27
+ ## Motivation
28
+ The gem has two main responsibilities:
29
+ 1. sign the request with proper Authorization headers
30
+ 2. provide a wrapper around the CCU V3 APIs
31
+
32
+ ### akamai-edgerid
33
+ There's an official gem by Akamai to sign HTTP headers called [akamai-edgegrid](https://github.com/akamai/AkamaiOPEN-edgegrid-ruby).
34
+ I've opted to go with my own implementation for the following reasons:
35
+ * the official gem is not written in idiomatic ruby
36
+ * Net::HTTP core class is extended, ignoring composition/decoration
37
+ * single responsibility principle is broken
38
+ * i prefer not relying on external dependencies when possible
39
+
40
+ ## Installation
41
+ Add this line to your application's Gemfile:
42
+ ```ruby
43
+ gem "akamai_ccu"
44
+ ```
45
+
46
+ And then execute:
47
+ ```shell
48
+ bundle
49
+ ```
50
+
51
+ Or install it yourself as:
52
+ ```shell
53
+ gem install akamai_ccu
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ ### Configuration
59
+ This gem requires you have a valid Akamai Luna Control Center account, enabled to use the CCU APIs.
60
+ Akamai relies on a credentials file with three secret keys and a dedicated host for API authorization.
61
+ Detailing how to get this file is out of the scope of this readme, check Akamai's [official documentation](https://developer.akamai.com/introduction/Conf_Client.html) for that.
62
+ Suffice to say have two main options:
63
+
64
+ #### edgerc
65
+ You can generate (by facility python scriot or by hand) a hidden file named `.edgerc`:
66
+ ```
67
+ [default]
68
+ client_secret = xxx=
69
+ host = akaa-baseurl-xxx-xxx.luna.akamaiapis.net/
70
+ access_token = akab-access-token-xxx-xxx
71
+ client_token = akab-client-token-xxx-xxx
72
+ max-body = 131072
73
+ ```
74
+
75
+ #### txt
76
+ You can download a plain text file directly from Luna Control Center `Manage APIs` page:
77
+ ```
78
+ client_secret = xxx=
79
+
80
+ host = akaa-baseurl-xxx-xxx.luna.akamaiapis.net/
81
+
82
+ access_token = akab-access-token-xxx-xxx
83
+
84
+ client_token = akab-client-token-xxx-xxx
85
+ ```
86
+
87
+ ### Inside your script
88
+ You can obviously use the gem directly inside your Ruby's script:
89
+
90
+ #### Secret
91
+ Once you've got your tokens file, you can instantiate the secret object aimed to generate the authorization header:
92
+ ```ruby
93
+ require "akamai_ccu"
94
+
95
+ # by .edgerc
96
+ secret = AkamaiCCU::Secret.by_edgerc("./.edgerc") # default to current working directory
97
+
98
+ # by txt file
99
+ secret = AkamaiCCU::Secret.by_txt("./tokens.txt")
100
+
101
+ # by specifying arguments
102
+ secret = AkamaiCCU::Secret.new(client_secret: "xxx=", host: "akaa-baseurl-xxx-xxx.luna.akamaiapis.net/", access_token: "akab-access-token-xxx-xxx", client_token: "akab-client-token-xxx-xxx", max_body: 131072)
103
+ ```
104
+
105
+ #### Invalidating
106
+ The CCU V3 APIs allow for invalidating the contents by URL or content provider (CP) code:
107
+ ```ruby
108
+ # invalidating resources on staging by url
109
+ AkamaiCCU::Wrapper.invalidate_by_url(%w[https://akaa-baseurl-xxx-xxx.luna.akamaiapis.net/index.html], secret)
110
+
111
+ # invalidating resources on production (mind the "!") by CP code
112
+ AkamaiCCU::Wrapper.invalidate_by_cpcode!([12345, 98765], secret)
113
+ ```
114
+
115
+ #### Deleting
116
+ You can also delete the contents by URL or CP code, just be aware of the consequences:
117
+ ```ruby
118
+ # deleting resources on staging by CP code
119
+ AkamaiCCU::Wrapper.delete_by_cpcode([12345, 98765], secret)
120
+
121
+ # deleting resources on production (mind the "!") by url
122
+ AkamaiCCU::Wrapper.delete_by_url!(%w[https://akaa-baseurl-xxx-xxx.luna.akamaiapis.net/*.js], secret)
123
+ ```
124
+
125
+ #### Reuse client
126
+ By default `Wrapper` class methods create a brand new Net::HTTP client on each call.
127
+ If this is an issue for you, you can rely on standard instance creation and just change the `endpoint` collaborator to switch API:
128
+ ```ruby
129
+ wrapper = AkamaiCCU::Wrapper.new(secret: secret, endpoint: AkamaiCCU::Endpoint.by_name("invalidate_by_url"))
130
+ wrapper.call(%w[https://akaa-baseurl-xxx-xxx.luna.akamaiapis.net/*.css])
131
+
132
+ # switch to deleting on production
133
+ wrapper.api = AkamaiCCU::Endpoint.by_name("delete_by_cpcode!")
134
+ wrapper.call([12345, 98765])
135
+ ```
136
+
137
+ #### Response
138
+ The Net::HTTP response is wrapped by an utility struct:
139
+ ```ruby
140
+ res = AkamaiCCU::Wrapper.invalidate_by_cpcode([12345, 98765], secret)
141
+ puts res
142
+ # status=201; detail=Request accepted; purge_id=e535071c-26b2-11e7-94d7-276f2f54d938; support_id=17PY1492793544958045-219026624; copletion_at=20170620T11:19:16+0000
143
+ ```
144
+
145
+ ### CLI
146
+ You can use the CLI by:
147
+
148
+ #### Help
149
+ Calling the help for the specific action:
150
+ ```shell
151
+ invalidate -h
152
+ Usage: invalidate --edgerc=./.edgerc --production --cp="12345, 98765"
153
+ -e, --edgerc=EDGERC Load secret by .edgerc file
154
+ -t, --txt=TXT Load secret by TXT file
155
+ -c, --cp=CP Specify contents by provider (CP) codes
156
+ -u, --url=URL Specify contents by URLs
157
+ --headers=HEADERS Specify HTTP headers to sign
158
+ -p, --production Purge on production network
159
+ -h, --help Prints this help
160
+ ```
161
+
162
+ #### invalidate
163
+ You can request for contents invalidation by calling:
164
+ ```shell
165
+ invalidate --edgerc=~/.edgerc \
166
+ --url="https://akaa-baseurl-xxx-xxx.luna.akamaiapis.net/*.css,https://akaa-baseurl-xxx-xxx.luna.akamaiapis.net/*.js" \
167
+ --production
168
+ ```
169
+
170
+ #### delete
171
+ You can request for contents deletion by calling:
172
+ ```shell
173
+ delete --txt=~/tokens.txt \
174
+ --cp=12345,98765 \
175
+ --headers=Accept,Content-Length
176
+ ```
177
+
178
+ #### Overwriting options
179
+ The CLI does allow only one option to specify the secret file and the content objects.
180
+ If multiple options for the same scope are provided, the program runs by giving precedence to:
181
+
182
+ ##### Secret file
183
+ The `edgerc` option has always precedence over the `txt` one:
184
+ ```shell
185
+ # will load secret from ~/.edgerc
186
+ invalidate --txt=~/tokens.txt \
187
+ --edgerc=~/.edgerc \
188
+ --cp=12345,98765
189
+ ```
190
+
191
+ ##### Content objects
192
+ The `cp` option has always precedence over the `url` one:
193
+ ```shell
194
+ # will invalidate by CP code
195
+ invalidate --txt=~/tokens.txt \
196
+ --url="https://akaa-baseurl-xxx-xxx.luna.akamaiapis.net/*.css,https://akaa-baseurl-xxx-xxx.luna.akamaiapis.net/*.js" \
197
+ --cp=12345,98765
198
+ ```
199
+
200
+ ### Possible Issues
201
+ It happens you can get a `bad request` response by Akamai like this:
202
+ ```shell
203
+ status=400; title=Bad request; detail=Invalid timestamp; request_id=2ce206fd; method=POST; requested_at=2017-06-21T12:33:10Z
204
+ ```
205
+
206
+ This happens since Akamai APIs only tolerate a clock skew of at most 30 seconds to defend against certain network attacks (described [here](https://community.akamai.com/docs/DOC-1336)).
207
+ In order to fix this annoying issue please do synchronize you server clock by:
208
+ * `NTP` if you are lucky to be on a UX server
209
+ * `manually` versus an atomic clock site (check Internet) by using your workstation GUI
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:spec) do |t|
5
+ t.libs << "spec"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["spec/**/*_spec.rb"]
8
+ end
9
+
10
+ task :default => :spec
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
3
+ require "akamai_ccu/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "akamai_ccu"
7
+ s.version = AkamaiCCU::VERSION
8
+ s.authors = ["costajob"]
9
+ s.email = ["costajob@gmail.com"]
10
+ s.summary = "Minimal high performant wrapper around Akamai CCU APIs"
11
+ s.homepage = "https://github.com/costajob/akamai_ccu"
12
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|test|s|features)/}) }
13
+ s.bindir = "bin"
14
+ s.executables = %w[invalidate delete]
15
+ s.require_paths = ["lib"]
16
+ s.license = "MIT"
17
+ s.required_ruby_version = ">= 2.2.2"
18
+
19
+ s.add_development_dependency "bundler", "~> 1.15"
20
+ s.add_development_dependency "rake", "~> 10.0"
21
+ s.add_development_dependency "minitest", "~> 5.0"
22
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "akamai_ccu"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/delete ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path("../../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require "akamai_ccu"
7
+
8
+ cli = AkamaiCCU::CLI.new(args: ARGV.clone, action: AkamaiCCU::Endpoint::Action::DELETE)
9
+ cli.call
data/bin/invalidate ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path("../../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require "akamai_ccu"
7
+
8
+ cli = AkamaiCCU::CLI.new(args: ARGV.clone, action: AkamaiCCU::Endpoint::Action::INVALIDATE)
9
+ cli.call
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,75 @@
1
+ require "optparse"
2
+ require "akamai_ccu/wrapper"
3
+
4
+ module AkamaiCCU
5
+ class CLI
6
+ attr_reader :network, :action
7
+
8
+ def initialize(args:, action:, io: STDOUT, wrapper_klass: Wrapper, secret_klass: Secret, endpoint_klass: Endpoint)
9
+ @args = args
10
+ @action = action
11
+ @io = io
12
+ @wrapper_klass = wrapper_klass
13
+ @secret_klass = secret_klass
14
+ @endpoint_klass = endpoint_klass
15
+ @network = Endpoint::Network::STAGING
16
+ end
17
+
18
+ def call
19
+ parser.parse!(@args)
20
+ return @io.puts(%q{Specify contents to purge either by cp codes or by urls}) unless @objects
21
+ return @io.puts(%q{Specify path to the secret file either by edgerc or by txt}) unless @secret
22
+ wrapper = @wrapper_klass.new(secret: secret, endpoint: endpoint, headers: Array(@headers))
23
+ @io.puts wrapper.call(@objects)
24
+ end
25
+
26
+ private def secret
27
+ return @secret_klass.by_txt(name: @secret) if File.extname(@secret) == ".txt"
28
+ @secret_klass.by_edgerc(name: @secret)
29
+ end
30
+
31
+ private def endpoint
32
+ @endpoint_klass.new(network, action, mode)
33
+ end
34
+
35
+ private def mode
36
+ return Endpoint::Mode::CPCODE if @objects.all? { |o| o.is_a?(Integer) }
37
+ Endpoint::Mode::URL
38
+ end
39
+
40
+ private def parser
41
+ OptionParser.new do |opts|
42
+ opts.banner = %Q{Usage: #{@action} --edgerc=./.edgerc --production --cp="12345, 98765"}
43
+
44
+ opts.on("-eEDGERC", "--edgerc=EDGERC", "Load secret by .edgerc file") do |secret|
45
+ @secret = secret
46
+ end
47
+
48
+ opts.on("-tTXT", "--txt=TXT", "Load secret by TXT file") do |secret|
49
+ @secret = secret
50
+ end
51
+
52
+ opts.on("-cCP", "--cp=CP", "Specify contents by provider (CP) codes") do |objects|
53
+ @objects = objects.split(",").map(&:strip).map(&:to_i)
54
+ end
55
+
56
+ opts.on("-uURL", "--url=URL", "Specify contents by URLs") do |objects|
57
+ @objects = objects.split(",").map(&:strip)
58
+ end
59
+
60
+ opts.on("--headers=HEADERS", "Specify HTTP headers to sign") do |headers|
61
+ @headers = headers.split(",").map(&:strip)
62
+ end
63
+
64
+ opts.on("-p", "--production", "Purge on production network") do |prod|
65
+ @network = Endpoint::Network::PRODUCTION
66
+ end
67
+
68
+ opts.on("-h", "--help", "Prints this help") do
69
+ @io.puts opts
70
+ exit
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,34 @@
1
+ require "net/http"
2
+ require "openssl"
3
+
4
+ module AkamaiCCU
5
+ class Client
6
+ attr_reader :net_klass, :host
7
+
8
+ def initialize(host:, net_klass: Net::HTTP)
9
+ @host = host
10
+ @net_klass = net_klass
11
+ end
12
+
13
+ def call(path: "/", method: POST, initheader: JSON_HEADER)
14
+ request(path, method, initheader)
15
+ yield @request if block_given?
16
+ Thread.new { http.request(@request) }.value
17
+ end
18
+
19
+ private def base_uri
20
+ @base_uri ||= URI("#{SSL}://#{host}")
21
+ end
22
+
23
+ private def http
24
+ @http ||= @net_klass.new(base_uri.host, base_uri.port).tap do |http|
25
+ http.use_ssl = true
26
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
27
+ end
28
+ end
29
+
30
+ private def request(path, klass = GET, initheader = nil)
31
+ @request ||= @net_klass.const_get(klass).new(base_uri.merge(path).to_s, initheader)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,63 @@
1
+ module AkamaiCCU
2
+ class Endpoint
3
+ BASE_PATH = "/ccu/v3"
4
+ SHEBANG = "!"
5
+
6
+ module Network
7
+ %w[staging production].each do |network|
8
+ const_set(network.upcase, network)
9
+ end
10
+ end
11
+
12
+ module Action
13
+ %w[invalidate delete].each do |action|
14
+ const_set("#{action}".upcase, action)
15
+ end
16
+ end
17
+
18
+ module Mode
19
+ %w[url cpcode].each do |mode|
20
+ const_set("#{mode}".upcase, mode)
21
+ end
22
+ end
23
+
24
+ def self.by_constants(network_const, action_const, mode_const)
25
+ network = Network.const_get(network_const)
26
+ action = Action.const_get(action_const)
27
+ mode = Mode.const_get(mode_const)
28
+ new(network, action, mode)
29
+ end
30
+
31
+ def self.by_name(name)
32
+ network = name.delete!(SHEBANG) ? Network::PRODUCTION : Network::STAGING
33
+ tokens = name.split("_")
34
+ tokens.delete("by")
35
+ action, mode = tokens
36
+ new(network, action, mode)
37
+ end
38
+
39
+ attr_reader :network, :action, :mode
40
+
41
+ def initialize(network, action, mode)
42
+ @network = network
43
+ @action = action
44
+ @mode = mode
45
+ end
46
+
47
+ def to_s
48
+ "#{@action}_by_#{@mode}#{shebang}"
49
+ end
50
+
51
+ def path
52
+ File.join(BASE_PATH, @action, @mode, @network)
53
+ end
54
+
55
+ private def production?
56
+ @network == Network::PRODUCTION
57
+ end
58
+
59
+ private def shebang
60
+ SHEBANG if production?
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,86 @@
1
+ require "time"
2
+
3
+ module AkamaiCCU
4
+ class Response
5
+ BAD_STATUS = 400
6
+
7
+ def self.factory(body)
8
+ response = new(body)
9
+ case response
10
+ when ->(res) { res.successful? }
11
+ Ack.new(body)
12
+ else
13
+ Error.new(body)
14
+ end
15
+ end
16
+
17
+ attr_reader :body, :status, :detail
18
+
19
+ def initialize(body = {})
20
+ @body = parse(body)
21
+ @status = @body.fetch("httpStatus") { @body.fetch("status", BAD_STATUS) }
22
+ @detail = @body["detail"]
23
+ end
24
+
25
+ def successful?
26
+ (@status.to_i / 100) == 2
27
+ end
28
+
29
+ private def parse(body)
30
+ return body if body.is_a? Hash
31
+ JSON.parse(body)
32
+ end
33
+ end
34
+
35
+ class Error < Response
36
+ attr_reader :type, :title, :request_id, :instance, :method, :server_ip, :client_ip
37
+
38
+ def initialize(body)
39
+ super(body)
40
+ @type = @body["type"]
41
+ @title = @body["title"]
42
+ @request_id = @body["requestId"]
43
+ @instance = @body["instance"]
44
+ @method = @body["method"]
45
+ @serverIp = @body["serverIp"]
46
+ @clientIp = @body["clientIp"]
47
+ @requested_at = @body["requestTime"]
48
+ end
49
+
50
+ def requested_at
51
+ return nil unless @requested_at
52
+ Time.parse(@requested_at)
53
+ end
54
+
55
+ def to_s
56
+ %W[status=#{@status}].tap do |a|
57
+ a << "title=#{@title}" if @title
58
+ a << "detail=#{@detail}" if @detail
59
+ a << "request_id=#{@request_id}" if @request_id
60
+ a << "method=#{@method}" if @method
61
+ a << "requested_at=#{@requested_at}" if @requested_at
62
+ end.join("; ")
63
+ end
64
+ end
65
+
66
+ class Ack < Response
67
+ attr_reader :purge_id, :estimated_secs, :support_id, :completion_at
68
+
69
+ def initialize(body, time = Time.now)
70
+ super(body)
71
+ @purge_id = @body["purgeId"]
72
+ @estimated_secs = @body["estimatedSeconds"]
73
+ @support_id = @body["supportId"]
74
+ @completion_at = time + @estimated_secs.to_i
75
+ end
76
+
77
+ def to_s
78
+ %W[status=#{@status}].tap do |a|
79
+ a << "detail=#{@detail}" if @detail
80
+ a << "purge_id=#{@purge_id}" if @purge_id
81
+ a << "support_id=#{@support_id}" if @support_id
82
+ a << "copletion_at=#{AkamaiCCU.format_utc(@completion_at)}" if @completion_at
83
+ end.join("; ")
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,61 @@
1
+ require "uri"
2
+ require "securerandom"
3
+
4
+ module AkamaiCCU
5
+ class Secret
6
+ DIGEST = "EG1-HMAC-SHA256"
7
+ EQUALITY = " = "
8
+
9
+ class << self
10
+ private def factory(opts, time)
11
+ new(client_secret: opts.fetch("client_secret"), host: opts.fetch("host"), access_token: opts.fetch("access_token"), client_token: opts.fetch("client_token"), max_body: opts.fetch("max-body", 2048), time: time)
12
+ end
13
+
14
+ def by_txt(name:, time: Time.now)
15
+ return unless File.exist?(name)
16
+ data = File.readlines(name).map(&:strip).reject(&:empty?).map do |entry|
17
+ entry.split(EQUALITY)
18
+ end
19
+ factory(Hash[data], time)
20
+ end
21
+
22
+ def by_edgerc(name: ".edgerc", time: Time.now)
23
+ return unless File.exist?(name)
24
+ data = File.readlines(name).map(&:strip)
25
+ data.shift
26
+ data.map! { |entry| entry.split(EQUALITY) }
27
+ factory(Hash[data], time)
28
+ end
29
+ end
30
+
31
+ attr_reader :host, :max_body
32
+
33
+ def initialize(client_secret:, host:, access_token:, client_token:, max_body: 2048, nonce: SecureRandom.uuid, time: Time.now)
34
+ @client_secret = client_secret
35
+ @host = URI(host)
36
+ @access_token = access_token
37
+ @client_token = client_token
38
+ @max_body = max_body.to_i
39
+ @nonce = nonce
40
+ @timestamp = AkamaiCCU.format_utc(time)
41
+ end
42
+
43
+ def touch
44
+ @timestamp = AkamaiCCU.format_utc(Time.now)
45
+ end
46
+
47
+ def signed_key
48
+ AkamaiCCU.sign_HMAC(key: @client_secret, data: @timestamp)
49
+ end
50
+
51
+ def auth_header
52
+ DIGEST.dup.tap do |header|
53
+ header << " "
54
+ header << "client_token=#{@client_token};"
55
+ header << "access_token=#{@access_token};"
56
+ header << "timestamp=#{@timestamp};"
57
+ header << "nonce=#{@nonce};"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,73 @@
1
+ require "forwardable"
2
+ require "uri"
3
+ require "akamai_ccu/secret"
4
+
5
+ module AkamaiCCU
6
+ class Signer
7
+ extend Forwardable
8
+
9
+ POST = "POST"
10
+ TAB = "\t"
11
+ HEADER_NAME = "signature"
12
+ HEADER_KEY = "Authorization"
13
+
14
+ def_delegators :@request, :body, :request_body_permitted?, :path, :method
15
+ def_delegators :@secret, :max_body, :auth_header, :signed_key
16
+
17
+ attr_reader :request
18
+
19
+ def initialize(request, secret = nil, headers = [])
20
+ @request = request
21
+ @secret = secret
22
+ @headers = Array(headers)
23
+ @url = URI(path)
24
+ end
25
+
26
+ def call!
27
+ return unless @secret
28
+ @request[HEADER_KEY] = signed_headers
29
+ end
30
+
31
+ private def canonical_headers
32
+ @headers.map do |header|
33
+ next unless @request.key?(header)
34
+ value = @request[header].strip.gsub(/\s+/, " ")
35
+ "#{header.downcase}:#{value}"
36
+ end.compact
37
+ end
38
+
39
+ private def body?
40
+ body && request_body_permitted?
41
+ end
42
+
43
+ private def signed_body
44
+ return "" unless body?
45
+ truncated = body[0...max_body]
46
+ AkamaiCCU.sign(truncated)
47
+ end
48
+
49
+ private def signature_data
50
+ @signature_data ||= [].tap do |data|
51
+ data << method
52
+ data << @url.scheme
53
+ data << @request.fetch("host") { @url.host }
54
+ data << @url.request_uri
55
+ data << canonical_headers.join(TAB)
56
+ data << signed_body
57
+ data << auth_header
58
+ end
59
+ end
60
+
61
+ private def signature
62
+ AkamaiCCU.sign_HMAC(key: signed_key, data: signature_data.join(TAB))
63
+ end
64
+
65
+ def signed_header
66
+ "#{HEADER_NAME}=#{signature}"
67
+ end
68
+
69
+ private def signed_headers
70
+ @signed_headers ||= auth_header << signed_header
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,3 @@
1
+ module AkamaiCCU
2
+ VERSION = "1.1.1"
3
+ end
@@ -0,0 +1,49 @@
1
+ require "akamai_ccu/client"
2
+ require "akamai_ccu/endpoint"
3
+ require "akamai_ccu/signer"
4
+ require "akamai_ccu/response"
5
+
6
+ module AkamaiCCU
7
+ class Wrapper
8
+ class << self
9
+ Endpoint::Network.constants.each do |network|
10
+ Endpoint::Action.constants.each do |action|
11
+ Endpoint::Mode.constants.each do |mode|
12
+ endpoint = Endpoint.by_constants(network, action, mode)
13
+ define_method(endpoint.to_s) do |objects = [], secret = nil, headers = [], &block|
14
+ wrapper = new(secret: secret, endpoint: endpoint, headers: headers)
15
+ block.call(wrapper) if block
16
+ wrapper.call(objects)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ attr_accessor :endpoint, :client_klass, :signer_klass, :response_klass
24
+
25
+ def initialize(secret:, endpoint:, headers: [],
26
+ client_klass: Client, signer_klass: Signer, response_klass: Response)
27
+ @secret = secret
28
+ @endpoint = endpoint
29
+ @client_klass = client_klass
30
+ @signer_klass = signer_klass
31
+ @response_klass = response_klass
32
+ @headers = headers
33
+ end
34
+
35
+ def call(objects = [])
36
+ return if objects.empty?
37
+ res = client.call(path: @endpoint.path) do |request|
38
+ request.body = { objects: objects }.to_json
39
+ @secret.touch
40
+ @signer_klass.new(request, @secret, @headers).call!
41
+ end
42
+ response_klass.factory(res.body)
43
+ end
44
+
45
+ private def client
46
+ @client ||= @client_klass.new(host: @secret.host)
47
+ end
48
+ end
49
+ end
data/lib/akamai_ccu.rb ADDED
@@ -0,0 +1,31 @@
1
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "openssl"
6
+ require "akamai_ccu/version"
7
+ require "akamai_ccu/wrapper"
8
+ require "akamai_ccu/cli"
9
+
10
+ module AkamaiCCU
11
+ extend self
12
+
13
+ GET = :Get
14
+ POST = :Post
15
+ SSL = "https"
16
+ JSON_HEADER = { "Content-Type" => "application/json" }
17
+
18
+ def format_utc(time)
19
+ time.utc.strftime("%Y%m%dT%H:%M:%S+0000")
20
+ end
21
+
22
+ def sign(data)
23
+ digest = OpenSSL::Digest::SHA256.new.digest(data)
24
+ Base64.encode64(digest).strip
25
+ end
26
+
27
+ def sign_HMAC(key:, data:)
28
+ digest = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, key, data)
29
+ Base64.encode64(digest).strip
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: akamai_ccu
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.1
5
+ platform: ruby
6
+ authors:
7
+ - costajob
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ description:
56
+ email:
57
+ - costajob@gmail.com
58
+ executables:
59
+ - invalidate
60
+ - delete
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".gitignore"
65
+ - ".travis.yml"
66
+ - Gemfile
67
+ - README.md
68
+ - Rakefile
69
+ - akamai_ccu.gemspec
70
+ - bin/console
71
+ - bin/delete
72
+ - bin/invalidate
73
+ - bin/setup
74
+ - lib/akamai_ccu.rb
75
+ - lib/akamai_ccu/cli.rb
76
+ - lib/akamai_ccu/client.rb
77
+ - lib/akamai_ccu/endpoint.rb
78
+ - lib/akamai_ccu/response.rb
79
+ - lib/akamai_ccu/secret.rb
80
+ - lib/akamai_ccu/signer.rb
81
+ - lib/akamai_ccu/version.rb
82
+ - lib/akamai_ccu/wrapper.rb
83
+ homepage: https://github.com/costajob/akamai_ccu
84
+ licenses:
85
+ - MIT
86
+ metadata: {}
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 2.2.2
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 2.6.8
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Minimal high performant wrapper around Akamai CCU APIs
107
+ test_files: []