akamai_ccu 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []