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 +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/README.md +209 -0
- data/Rakefile +10 -0
- data/akamai_ccu.gemspec +22 -0
- data/bin/console +14 -0
- data/bin/delete +9 -0
- data/bin/invalidate +9 -0
- data/bin/setup +8 -0
- data/lib/akamai_ccu/cli.rb +75 -0
- data/lib/akamai_ccu/client.rb +34 -0
- data/lib/akamai_ccu/endpoint.rb +63 -0
- data/lib/akamai_ccu/response.rb +86 -0
- data/lib/akamai_ccu/secret.rb +61 -0
- data/lib/akamai_ccu/signer.rb +73 -0
- data/lib/akamai_ccu/version.rb +3 -0
- data/lib/akamai_ccu/wrapper.rb +49 -0
- data/lib/akamai_ccu.rb +31 -0
- metadata +107 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
data/akamai_ccu.gemspec
ADDED
@@ -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
data/bin/invalidate
ADDED
data/bin/setup
ADDED
@@ -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,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: []
|