distributed-press-api-client 0.2.4 → 0.3.0rc1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9655c1c4310b5c3d7b121b9984ba13eff6989deaf1056bfe3a0dd8b2c0ae493b
4
- data.tar.gz: 4933a217789771803d88f9c6b92d09119846730ab61543e2789c9a1348796ef9
3
+ metadata.gz: 2552a7612cdbbf7fe48afacf1327764e8f3f25a888d7bfd9f852b54a3fb5fa67
4
+ data.tar.gz: 59b252609b9d442aee543d1e5eabb642a6321c749c4936d35ad5d5e40038df3e
5
5
  SHA512:
6
- metadata.gz: 0a348613279cf9cd9753561136bcebaae7729ff112ad20be435e966b62336a899da72fcd5fd736318103174442b1e931ba7c566b01fdcea876954a6a2e922785
7
- data.tar.gz: 9f2504559bf4c6b7542ec50f649dbeb290a8fe8aa2add595a26a84ef45015d38801526a9d765ba7269a26f2f68f00373346951cbd3b165b684431c39383989ad
6
+ metadata.gz: 7d81f9c8bbdfa947f9c08b20e9e6214c0b1852987384eb7b5d4c5c757d91914f83812b781bf567122ad7790403137c3c1a8459f19ef5331e1b90bf7910887cbd
7
+ data.tar.gz: 909d13d6fba811597be93e00aecd24cb6961b65baa7cd0b669e7029fbdb34b9dc7e899dee6dfc8b2ec4575c6a4599d746b9a3182aa6ac50ad509ed51d0dbb5f2
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+ require 'openssl'
5
+ require 'time'
6
+ require 'digest/sha2'
7
+ require 'uri'
8
+
9
+ class DistributedPress
10
+ module V1
11
+ module Social
12
+ # Social Distributed Press APIv1 client
13
+ #
14
+ # Inspired by Mastodon's Request
15
+ #
16
+ # @todo It'd be nice to implement this on HTTParty itself
17
+ # @see {https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb}
18
+ class Client
19
+ include ::HTTParty
20
+
21
+ # Signing algorithm
22
+ #
23
+ # @todo is it possible to use other algorithms?
24
+ ALGORITHM = 'rsa-sha256'
25
+ # Required by HTTP Signatures
26
+ REQUEST_TARGET = '(request-target)'
27
+ # Headers included in the signature
28
+ SIGNABLE_HEADERS = [REQUEST_TARGET, 'Host', 'Date', 'Digest']
29
+
30
+ # API URL
31
+ # @return [String]
32
+ attr_reader :url
33
+
34
+ # RSA key size
35
+ #
36
+ # @return [Integer]
37
+ attr_reader :key_size
38
+
39
+ # Public key URL
40
+ #
41
+ # @return [String]
42
+ attr_reader :public_key_url
43
+
44
+ # @param :url [String] Social Distributed Press URL
45
+ # @param :public_key [String] URL where public key is available
46
+ # @param :private_key_pem [String] RSA Private key, PEM encoded
47
+ # @param :key_size [Integer]
48
+ # @param :logger [Logger]
49
+ def initialize(url: 'https://social.distributed.press', public_key_url:, private_key_pem: nil, key_size: 2048, logger: nil)
50
+ self.class.default_options[:base_uri] = @url = HTTParty.normalize_base_uri(url)
51
+ self.class.default_options[:logger] = logger
52
+ self.class.default_options[:log_level] = :debug
53
+
54
+ @public_key_url = public_key_url
55
+ @key_size = key_size
56
+ @private_key_pem = private_key_pem
57
+ end
58
+
59
+ # POST request
60
+ #
61
+ # @todo Use DRY-schemas
62
+ # @param endpoint [String]
63
+ # @param body [Hash]
64
+ # @return [Hash]
65
+ def post(endpoint:, body:)
66
+ body = body.to_json
67
+ headers = default_headers
68
+
69
+ add_request_specific_headers! headers, 'post', endpoint
70
+ checksum_body! body, headers
71
+ sign_headers! headers
72
+
73
+ self.class.post(endpoint, body: body, headers: headers)
74
+ end
75
+
76
+ # Loads or generates a private key
77
+ #
78
+ # @return [OpenSSL::PKey::RSA]
79
+ def private_key
80
+ @private_key ||= OpenSSL::PKey::RSA.new(@private_key_pem || key_size)
81
+ end
82
+
83
+ # Public key
84
+ #
85
+ # @return [OpenSSL::PKey::RSA]
86
+ def public_key
87
+ private_key.public_key
88
+ end
89
+
90
+ # Host
91
+ #
92
+ # @return [String]
93
+ def host
94
+ @host ||= URI.parse(url).host
95
+ end
96
+
97
+ private
98
+
99
+ def default_headers
100
+ {
101
+ 'User-Agent' => "DistributedPress/#{DistributedPress::VERSION}",
102
+ 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
103
+ 'Host' => host
104
+ }
105
+ end
106
+
107
+ # HTTP Signatures
108
+ #
109
+ # @see {https://docs.joinmastodon.org/spec/security/}
110
+ # @param :headers [Hash]
111
+ def sign_headers!(headers)
112
+ headers['Signature'] = {
113
+ 'keyId' => public_key_url,
114
+ 'algorithm' => ALGORITHM,
115
+ 'headers' => signable_headers(headers),
116
+ 'signature' => signed_headers(headers)
117
+ }.map do |key, value|
118
+ "#{key}=\"#{value}\""
119
+ end.join(',')
120
+
121
+ headers.delete REQUEST_TARGET
122
+
123
+ nil
124
+ end
125
+
126
+ # List of headers to be signed, removing headers that don't
127
+ # exist on the request.
128
+ #
129
+ # @return [String]
130
+ def signable_headers(headers)
131
+ (SIGNABLE_HEADERS & headers.keys).join(' ').downcase
132
+ end
133
+
134
+ # Sign headers
135
+ #
136
+ # @param :headers [Hash]
137
+ # @return [String]
138
+ def signed_headers(headers)
139
+ Base64.strict_encode64(
140
+ private_key.sign(
141
+ OpenSSL::Digest.new('SHA256'),
142
+ signature_content(headers)
143
+ )
144
+ )
145
+ end
146
+
147
+ # Generates a string to be signed
148
+ #
149
+ # @param :headers [Hash]
150
+ # @return [String]
151
+ def signature_content(headers)
152
+ headers.slice(*SIGNABLE_HEADERS).map do |key, value|
153
+ "#{key.downcase}: #{value}"
154
+ end.join("\n")
155
+ end
156
+
157
+ # Generate a checksum for the request body
158
+ #
159
+ # @param :body [String]
160
+ # @param :headers [Hash]
161
+ def checksum_body!(body, headers)
162
+ headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest body}"
163
+
164
+ nil
165
+ end
166
+
167
+ # Headers specific to a single request
168
+ #
169
+ # @param :headers [Hash] Headers
170
+ # @param :verb [String] HTTP verb
171
+ # @param :endpoint [String] Path
172
+ def add_request_specific_headers!(headers, verb, endpoint)
173
+ headers['Date'] = Time.now.utc.httpdate
174
+ headers[REQUEST_TARGET] = "#{verb} #{endpoint}"
175
+
176
+ nil
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'social/client'
4
+
5
+ class DistributedPress
6
+ module V1
7
+ # Social Distributed Press APIv1
8
+ module Social
9
+ end
10
+ end
11
+ end
@@ -3,5 +3,5 @@
3
3
  # API client
4
4
  class DistributedPress
5
5
  # Version
6
- VERSION = '0.2.4'
6
+ VERSION = '0.3.0rc1'
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: distributed-press-api-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - f
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-30 00:00:00.000000000 Z
11
+ date: 2023-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -277,6 +277,8 @@ files:
277
277
  - lib/distributed_press/v1/schemas/token_header.rb
278
278
  - lib/distributed_press/v1/schemas/token_payload.rb
279
279
  - lib/distributed_press/v1/schemas/update_site.rb
280
+ - lib/distributed_press/v1/social.rb
281
+ - lib/distributed_press/v1/social/client.rb
280
282
  - lib/distributed_press/v1/token.rb
281
283
  - lib/distributed_press/version.rb
282
284
  - lib/jekyll-distributed-press-v0.rb
@@ -307,9 +309,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
307
309
  version: '2.7'
308
310
  required_rubygems_version: !ruby/object:Gem::Requirement
309
311
  requirements:
310
- - - ">="
312
+ - - ">"
311
313
  - !ruby/object:Gem::Version
312
- version: '0'
314
+ version: 1.3.1
313
315
  requirements: []
314
316
  rubygems_version: 3.3.26
315
317
  signing_key: