dnsign 0.0.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.
@@ -0,0 +1,70 @@
1
+ require 'json'
2
+ require 'timers'
3
+ require 'net/http'
4
+
5
+ require 'dnsign/error'
6
+ require 'dnsign/ip_resolver'
7
+ require 'dnsign/dns_service'
8
+
9
+ module Dnsign
10
+ class ResolveUpdateLoop
11
+
12
+ def initialize(domain, dns_service, ip_resolver, opts={})
13
+ @domain = domain
14
+ @ip_resolver = ip_resolver
15
+ @dns_service = dns_service
16
+ @timers = Timers::Group.new
17
+
18
+ @verbose = opts[:verbose].nil? ? true : opts[:verbose]
19
+ @interval = opts[:interval] || 300
20
+ end
21
+
22
+ def kickoff
23
+ if @interval > 0
24
+ @timer = @timers.every( @interval ) { update }
25
+ loop { @timers.wait }
26
+ end
27
+ end
28
+
29
+ def update
30
+ real_ip = resolve_real_ip
31
+ dns_ip = retrieve_dns_ip
32
+
33
+ if real_ip != dns_ip
34
+ update_dns_ip(real_ip)
35
+ log "UPDATE: Public #{real_ip} differs from the resolved #{dns_ip}"
36
+ true
37
+ else
38
+ log "SKIPPING: Public #{real_ip} matches with the resolved #{dns_ip}"
39
+ false
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def resolve_real_ip
46
+ @ip_resolver.resolve
47
+ rescue InvalidResponseFromIpResolver => e
48
+ log e
49
+ end
50
+
51
+ def retrieve_dns_ip
52
+ @dns_service.retrieve_ip @domain
53
+ rescue RecordDoesNotExists => e
54
+ log e
55
+ end
56
+
57
+ def update_dns_ip(real_ip)
58
+ @dns_service.update_ip @domain, real_ip
59
+ rescue DomainDoesNotExists => e
60
+ log e
61
+ end
62
+
63
+ def log(msg)
64
+ if @verbose == true
65
+ puts ["[#{Time.now.utc}]", msg].join ' '
66
+ end
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,3 @@
1
+ module Dnsign
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,28 @@
1
+ ### Monkey patch
2
+ #
3
+ # DropletKit doesn't permit updating of 'data' attribute of domain record,
4
+ # even if DigitalOcean API documention says it should.
5
+ #
6
+ # https://github.com/digitalocean/droplet_kit/blob/master/lib/droplet_kit/mappings/domain_record_mapping.rb#L12
7
+ # https://developers.digitalocean.com/documentation/v2/#update-a-domain-record
8
+ # https://github.com/digitalocean/droplet_kit/issues/44
9
+ #
10
+
11
+ module DropletKit
12
+ class DomainRecordMapping
13
+ include Kartograph::DSL
14
+
15
+ kartograph do
16
+ mapping DomainRecord
17
+ root_key plural: 'domain_records', singular: 'domain_record', scopes: [:read]
18
+
19
+ property :id, scopes: [:read]
20
+ property :type, scopes: [:read, :create]
21
+ property :name, scopes: [:read, :create, :update]
22
+ property :data, scopes: [:read, :create, :update] # :update added
23
+ property :priority, scopes: [:read, :create]
24
+ property :port, scopes: [:read, :create]
25
+ property :weight, scopes: [:read, :create]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,43 @@
1
+ require 'dnsign/dns_services/digital_ocean'
2
+
3
+ RSpec.describe DnsServices::DigitalOcean do
4
+
5
+ before :all do
6
+ @service = DnsServices::DigitalOcean.new access_token: '...'
7
+ end
8
+
9
+ describe '#update_ip' do
10
+
11
+ it 'creates new domain record' do
12
+ VCR.use_cassette('digital_ocean_create_domain_record') do
13
+ ip = @service.update_ip 'foo.floatingpoint.io', '127.0.0.1'
14
+ expect(ip).to eq('127.0.0.1')
15
+ end
16
+ end
17
+
18
+ it 'updates existing domain record' do
19
+ VCR.use_cassette('digital_ocean_update_domain_record') do
20
+ ip = @service.update_ip 'foo.floatingpoint.io', '127.0.1.1'
21
+ expect(ip).to eq('127.0.1.1')
22
+ end
23
+ end
24
+ end
25
+
26
+ describe '#retrieve_ip' do
27
+
28
+ it 'fetches existing domain record and returns its IP' do
29
+ VCR.use_cassette('digital_ocean_read_domain_record') do
30
+ ip = @service.retrieve_ip 'foo.floatingpoint.io'
31
+ expect(ip).to eq('127.0.0.1')
32
+ end
33
+ end
34
+
35
+ it 'returns nil for nonexisting record name' do
36
+ VCR.use_cassette('digital_ocean_read_domain_record') do
37
+ ip = @service.retrieve_ip 'bar.floatingpoint.io'
38
+ expect(ip).to be_nil
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ require 'dnsign/dns_services/linode'
2
+
3
+ RSpec.describe DnsServices::Linode do
4
+
5
+ before :all do
6
+ @service = DnsServices::Linode.new access_token: '...'
7
+ end
8
+
9
+ describe '#update_ip' do
10
+
11
+ it 'creates new domain record' do
12
+ VCR.use_cassette('linode_create_domain_record') do
13
+ ip = @service.update_ip 'foo.floatingpoint.io', '127.0.0.1'
14
+ expect(ip).to eq('127.0.0.1')
15
+ end
16
+ end
17
+
18
+ it 'updates existing domain record' do
19
+ VCR.use_cassette('linode_update_domain_record') do
20
+ ip = @service.update_ip 'foo.floatingpoint.io', '127.0.1.1'
21
+ expect(ip).to eq('127.0.1.1')
22
+ end
23
+ end
24
+ end
25
+
26
+ describe '#retrieve_ip' do
27
+
28
+ it 'fetches existing domain record and returns its IP' do
29
+ VCR.use_cassette('linode_read_domain_record') do
30
+ ip = @service.retrieve_ip 'foo.floatingpoint.io'
31
+ expect(ip).to eq('127.0.0.1')
32
+ end
33
+ end
34
+
35
+ it 'returns nil for nonexisting record name' do
36
+ VCR.use_cassette('linode_read_domain_record') do
37
+ ip = @service.retrieve_ip 'bar.floatingpoint.io'
38
+ expect(ip).to be_nil
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,129 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://api.digitalocean.com/v2/domains/floatingpoint.io/records
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Content-Type:
11
+ - application/json
12
+ Authorization:
13
+ - Bearer ...
14
+ User-Agent:
15
+ - Faraday v0.9.1
16
+ Accept-Encoding:
17
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
18
+ Accept:
19
+ - "*/*"
20
+ response:
21
+ status:
22
+ code: 200
23
+ message: OK
24
+ headers:
25
+ Server:
26
+ - cloudflare-nginx
27
+ Date:
28
+ - Mon, 13 Apr 2015 19:36:52 GMT
29
+ Content-Type:
30
+ - application/json; charset=utf-8
31
+ Transfer-Encoding:
32
+ - chunked
33
+ Connection:
34
+ - keep-alive
35
+ Set-Cookie:
36
+ - __cfduid=d6dcb738dedb3817ef26bae5640926e731428953811; expires=Tue, 12-Apr-16
37
+ 19:36:51 GMT; path=/; domain=.digitalocean.com; HttpOnly
38
+ Status:
39
+ - 200 OK
40
+ X-Frame-Options:
41
+ - SAMEORIGIN
42
+ X-Xss-Protection:
43
+ - 1; mode=block
44
+ X-Content-Type-Options:
45
+ - nosniff
46
+ Ratelimit-Limit:
47
+ - '5000'
48
+ Ratelimit-Remaining:
49
+ - '4994'
50
+ Ratelimit-Reset:
51
+ - '1428957146'
52
+ Cache-Control:
53
+ - max-age=0, private, must-revalidate
54
+ X-Request-Id:
55
+ - d9dfb763-61af-496c-a227-56b524c3ee73
56
+ X-Runtime:
57
+ - '0.135043'
58
+ Cf-Ray:
59
+ - 1d699f4b5efa02db-AMS
60
+ body:
61
+ encoding: UTF-8
62
+ string: '{"domain_records":[{"id":1851008,"type":"NS","name":"@","data":"ns1.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1851009,"type":"NS","name":"@","data":"ns2.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1851010,"type":"NS","name":"@","data":"ns3.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1851925,"type":"A","name":"@","data":"178.62.198.200","priority":null,"port":null,"weight":null},{"id":1851934,"type":"A","name":"floatingpoint.io","data":"178.62.198.200","priority":null,"port":null,"weight":null},{"id":1851939,"type":"CNAME","name":"www","data":"@","priority":null,"port":null,"weight":null},{"id":1929343,"type":"MX","name":"@","data":"aspmx.l.google.com","priority":1,"port":null,"weight":null},{"id":1929344,"type":"MX","name":"@","data":"alt1.aspmx.l.google.com","priority":5,"port":null,"weight":null},{"id":1929345,"type":"MX","name":"@","data":"alt2.aspmx.l.google.com","priority":5,"port":null,"weight":null},{"id":1929346,"type":"MX","name":"@","data":"aspmx2.googlemail.com","priority":10,"port":null,"weight":null},{"id":1929347,"type":"MX","name":"@","data":"aspmx3.googlemail.com","priority":10,"port":null,"weight":null},{"id":3671318,"type":"CNAME","name":"jabber","data":"@","priority":null,"port":null,"weight":null},{"id":3671325,"type":"SRV","name":"_xmpp-client._tcp","data":"jabber","priority":0,"port":5222,"weight":5},{"id":3671326,"type":"SRV","name":"_xmpp-server._tcp","data":"jabber","priority":0,"port":5269,"weight":5},{"id":5529034,"type":"CNAME","name":"structuredmind","data":"@","priority":null,"port":null,"weight":null}],"links":{},"meta":{"total":15}}'
63
+ http_version:
64
+ recorded_at: Mon, 13 Apr 2015 19:36:51 GMT
65
+ - request:
66
+ method: post
67
+ uri: https://api.digitalocean.com/v2/domains/floatingpoint.io/records
68
+ body:
69
+ encoding: UTF-8
70
+ string: '{"type":"A","name":"foo","data":"127.0.0.1","priority":null,"port":null,"weight":null}'
71
+ headers:
72
+ Content-Type:
73
+ - application/json
74
+ Authorization:
75
+ - Bearer 740ed6dd3e97e498892b2c2d0fd74376dc259c2b4cf898b168fbaa650171b732
76
+ User-Agent:
77
+ - Faraday v0.9.1
78
+ Accept-Encoding:
79
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
80
+ Accept:
81
+ - "*/*"
82
+ response:
83
+ status:
84
+ code: 201
85
+ message: Created
86
+ headers:
87
+ Server:
88
+ - cloudflare-nginx
89
+ Date:
90
+ - Mon, 13 Apr 2015 19:36:54 GMT
91
+ Content-Type:
92
+ - application/json; charset=utf-8
93
+ Transfer-Encoding:
94
+ - chunked
95
+ Connection:
96
+ - keep-alive
97
+ Set-Cookie:
98
+ - __cfduid=d6aa26e5acbbfad1f7cd829526d828c9c1428953813; expires=Tue, 12-Apr-16
99
+ 19:36:53 GMT; path=/; domain=.digitalocean.com; HttpOnly
100
+ Status:
101
+ - 201 Created
102
+ X-Frame-Options:
103
+ - SAMEORIGIN
104
+ X-Xss-Protection:
105
+ - 1; mode=block
106
+ X-Content-Type-Options:
107
+ - nosniff
108
+ Ratelimit-Limit:
109
+ - '5000'
110
+ Ratelimit-Remaining:
111
+ - '4993'
112
+ Ratelimit-Reset:
113
+ - '1428957146'
114
+ Etag:
115
+ - '"89d865317d5ead6eb139000676801d45"'
116
+ Cache-Control:
117
+ - max-age=0, private, must-revalidate
118
+ X-Request-Id:
119
+ - 4f54b117-79cb-4b13-be2b-929cb24d9362
120
+ X-Runtime:
121
+ - '0.240406'
122
+ Cf-Ray:
123
+ - 1d699f5961e700dd-AMS
124
+ body:
125
+ encoding: UTF-8
126
+ string: '{"domain_record":{"id":6221827,"type":"A","name":"foo","data":"127.0.0.1","priority":null,"port":null,"weight":null}}'
127
+ http_version:
128
+ recorded_at: Mon, 13 Apr 2015 19:36:53 GMT
129
+ recorded_with: VCR 2.9.3
@@ -0,0 +1,65 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://api.digitalocean.com/v2/domains/floatingpoint.io/records
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Content-Type:
11
+ - application/json
12
+ Authorization:
13
+ - Bearer ...
14
+ User-Agent:
15
+ - Faraday v0.9.1
16
+ Accept-Encoding:
17
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
18
+ Accept:
19
+ - "*/*"
20
+ response:
21
+ status:
22
+ code: 200
23
+ message: OK
24
+ headers:
25
+ Server:
26
+ - cloudflare-nginx
27
+ Date:
28
+ - Mon, 13 Apr 2015 19:37:52 GMT
29
+ Content-Type:
30
+ - application/json; charset=utf-8
31
+ Transfer-Encoding:
32
+ - chunked
33
+ Connection:
34
+ - keep-alive
35
+ Set-Cookie:
36
+ - __cfduid=de5d21637edd51517b39f58b486a037bc1428953871; expires=Tue, 12-Apr-16
37
+ 19:37:51 GMT; path=/; domain=.digitalocean.com; HttpOnly
38
+ Status:
39
+ - 200 OK
40
+ X-Frame-Options:
41
+ - SAMEORIGIN
42
+ X-Xss-Protection:
43
+ - 1; mode=block
44
+ X-Content-Type-Options:
45
+ - nosniff
46
+ Ratelimit-Limit:
47
+ - '5000'
48
+ Ratelimit-Remaining:
49
+ - '4990'
50
+ Ratelimit-Reset:
51
+ - '1428957146'
52
+ Cache-Control:
53
+ - max-age=0, private, must-revalidate
54
+ X-Request-Id:
55
+ - 92b081c0-f198-461c-a23e-e4db62582a7e
56
+ X-Runtime:
57
+ - '0.153754'
58
+ Cf-Ray:
59
+ - 1d69a0c204ab0c17-AMS
60
+ body:
61
+ encoding: UTF-8
62
+ string: '{"domain_records":[{"id":1851008,"type":"NS","name":"@","data":"ns1.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1851009,"type":"NS","name":"@","data":"ns2.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1851010,"type":"NS","name":"@","data":"ns3.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1851925,"type":"A","name":"@","data":"178.62.198.200","priority":null,"port":null,"weight":null},{"id":1851934,"type":"A","name":"floatingpoint.io","data":"178.62.198.200","priority":null,"port":null,"weight":null},{"id":1851939,"type":"CNAME","name":"www","data":"@","priority":null,"port":null,"weight":null},{"id":1929343,"type":"MX","name":"@","data":"aspmx.l.google.com","priority":1,"port":null,"weight":null},{"id":1929344,"type":"MX","name":"@","data":"alt1.aspmx.l.google.com","priority":5,"port":null,"weight":null},{"id":1929345,"type":"MX","name":"@","data":"alt2.aspmx.l.google.com","priority":5,"port":null,"weight":null},{"id":1929346,"type":"MX","name":"@","data":"aspmx2.googlemail.com","priority":10,"port":null,"weight":null},{"id":1929347,"type":"MX","name":"@","data":"aspmx3.googlemail.com","priority":10,"port":null,"weight":null},{"id":3671318,"type":"CNAME","name":"jabber","data":"@","priority":null,"port":null,"weight":null},{"id":3671325,"type":"SRV","name":"_xmpp-client._tcp","data":"jabber","priority":0,"port":5222,"weight":5},{"id":3671326,"type":"SRV","name":"_xmpp-server._tcp","data":"jabber","priority":0,"port":5269,"weight":5},{"id":5529034,"type":"CNAME","name":"structuredmind","data":"@","priority":null,"port":null,"weight":null},{"id":6221827,"type":"A","name":"foo","data":"127.0.0.1","priority":null,"port":null,"weight":null}],"links":{},"meta":{"total":16}}'
63
+ http_version:
64
+ recorded_at: Mon, 13 Apr 2015 19:37:52 GMT
65
+ recorded_with: VCR 2.9.3
@@ -0,0 +1,129 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://api.digitalocean.com/v2/domains/floatingpoint.io/records
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Content-Type:
11
+ - application/json
12
+ Authorization:
13
+ - Bearer ...
14
+ User-Agent:
15
+ - Faraday v0.9.12
16
+ Accept-Encoding:
17
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
18
+ Accept:
19
+ - "*/*"
20
+ response:
21
+ status:
22
+ code: 200
23
+ message: OK
24
+ headers:
25
+ Server:
26
+ - cloudflare-nginx
27
+ Date:
28
+ - Thu, 16 Apr 2015 19:29:20 GMT
29
+ Content-Type:
30
+ - application/json; charset=utf-8
31
+ Transfer-Encoding:
32
+ - chunked
33
+ Connection:
34
+ - keep-alive
35
+ Set-Cookie:
36
+ - __cfduid=df01ae98350a0aa7920da8d212da6e7311429212560; expires=Fri, 15-Apr-16
37
+ 19:29:20 GMT; path=/; domain=.digitalocean.com; HttpOnly
38
+ Status:
39
+ - 200 OK
40
+ X-Frame-Options:
41
+ - SAMEORIGIN
42
+ X-Xss-Protection:
43
+ - 1; mode=block
44
+ X-Content-Type-Options:
45
+ - nosniff
46
+ Ratelimit-Limit:
47
+ - '5000'
48
+ Ratelimit-Remaining:
49
+ - '4999'
50
+ Ratelimit-Reset:
51
+ - '1429216160'
52
+ Cache-Control:
53
+ - max-age=0, private, must-revalidate
54
+ X-Request-Id:
55
+ - ccf97b83-481c-4cd1-bac0-3bf5e8c89d10
56
+ X-Runtime:
57
+ - '0.137237'
58
+ Cf-Ray:
59
+ - 1d824c65923b0557-VIE
60
+ body:
61
+ encoding: UTF-8
62
+ string: '{"domain_records":[{"id":1851008,"type":"NS","name":"@","data":"ns1.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1851009,"type":"NS","name":"@","data":"ns2.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1851010,"type":"NS","name":"@","data":"ns3.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1851925,"type":"A","name":"@","data":"178.62.198.200","priority":null,"port":null,"weight":null},{"id":1851934,"type":"A","name":"floatingpoint.io","data":"178.62.198.200","priority":null,"port":null,"weight":null},{"id":1851939,"type":"CNAME","name":"www","data":"@","priority":null,"port":null,"weight":null},{"id":1929343,"type":"MX","name":"@","data":"aspmx.l.google.com","priority":1,"port":null,"weight":null},{"id":1929344,"type":"MX","name":"@","data":"alt1.aspmx.l.google.com","priority":5,"port":null,"weight":null},{"id":1929345,"type":"MX","name":"@","data":"alt2.aspmx.l.google.com","priority":5,"port":null,"weight":null},{"id":1929346,"type":"MX","name":"@","data":"aspmx2.googlemail.com","priority":10,"port":null,"weight":null},{"id":1929347,"type":"MX","name":"@","data":"aspmx3.googlemail.com","priority":10,"port":null,"weight":null},{"id":3671318,"type":"CNAME","name":"jabber","data":"@","priority":null,"port":null,"weight":null},{"id":3671325,"type":"SRV","name":"_xmpp-client._tcp","data":"jabber","priority":0,"port":5222,"weight":5},{"id":3671326,"type":"SRV","name":"_xmpp-server._tcp","data":"jabber","priority":0,"port":5269,"weight":5},{"id":5529034,"type":"CNAME","name":"structuredmind","data":"@","priority":null,"port":null,"weight":null},{"id":6232286,"type":"A","name":"office","data":"93.136.62.55","priority":null,"port":null,"weight":null}],"links":{},"meta":{"total":16}}'
63
+ http_version:
64
+ recorded_at: Thu, 16 Apr 2015 19:29:20 GMT
65
+ - request:
66
+ method: post
67
+ uri: https://api.digitalocean.com/v2/domains/floatingpoint.io/records
68
+ body:
69
+ encoding: UTF-8
70
+ string: '{"type":"A","name":"foo","data":"127.0.1.1","priority":null,"port":null,"weight":null}'
71
+ headers:
72
+ Content-Type:
73
+ - application/json
74
+ Authorization:
75
+ - Bearer 740ed6dd3e97e498892b2c2d0fd74376dc259c2b4cf898b168fbaa650171b732
76
+ User-Agent:
77
+ - Faraday v0.9.1
78
+ Accept-Encoding:
79
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
80
+ Accept:
81
+ - "*/*"
82
+ response:
83
+ status:
84
+ code: 201
85
+ message: Created
86
+ headers:
87
+ Server:
88
+ - cloudflare-nginx
89
+ Date:
90
+ - Thu, 16 Apr 2015 19:29:22 GMT
91
+ Content-Type:
92
+ - application/json; charset=utf-8
93
+ Transfer-Encoding:
94
+ - chunked
95
+ Connection:
96
+ - keep-alive
97
+ Set-Cookie:
98
+ - __cfduid=d9a857770dc4aca53f30819ba4871437d1429212561; expires=Fri, 15-Apr-16
99
+ 19:29:21 GMT; path=/; domain=.digitalocean.com; HttpOnly
100
+ Status:
101
+ - 201 Created
102
+ X-Frame-Options:
103
+ - SAMEORIGIN
104
+ X-Xss-Protection:
105
+ - 1; mode=block
106
+ X-Content-Type-Options:
107
+ - nosniff
108
+ Ratelimit-Limit:
109
+ - '5000'
110
+ Ratelimit-Remaining:
111
+ - '4998'
112
+ Ratelimit-Reset:
113
+ - '1429216160'
114
+ Etag:
115
+ - '"154a5bdcde3e91c31f170b4680f057b6"'
116
+ Cache-Control:
117
+ - max-age=0, private, must-revalidate
118
+ X-Request-Id:
119
+ - 3bbcd5f5-d1e8-490f-b6e8-7d431a563aa3
120
+ X-Runtime:
121
+ - '0.269614'
122
+ Cf-Ray:
123
+ - 1d824c6b973f0a3c-VIE
124
+ body:
125
+ encoding: UTF-8
126
+ string: '{"domain_record":{"id":6261986,"type":"A","name":"foo","data":"127.0.1.1","priority":null,"port":null,"weight":null}}'
127
+ http_version:
128
+ recorded_at: Thu, 16 Apr 2015 19:29:22 GMT
129
+ recorded_with: VCR 2.9.3