distributed-press-api-client 0.2.3 → 0.3.0rc0
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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25fa00e1c0a4044f03953f30018830d9cc3e934445be8bd1e72004e0c1f5b1bf
|
4
|
+
data.tar.gz: 0ddb5921a185fb2172669957b3a22d8619fa6a0d2d53fe33a5bb10372706a1b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 895ce13e70b28366fa412be512eeb075a179239a5eb7e45e302b58147baa55bccaba37505a93fdc97cef127d63faa655163eaa33c740bf2e5c334a3ab7879a5a
|
7
|
+
data.tar.gz: dcd6f8f7a6471ae979199de943350cff2b37ad5c148dbe8d143c6234cbb93e48dbc83b2794476bb0e7d193aff828dd591f96e747822bd085d89c9ef9c0a995e7
|
@@ -0,0 +1,178 @@
|
|
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
|
+
def initialize(url: 'https://social.distributed.press', public_key_url:, private_key_pem: nil, key_size: 2048)
|
49
|
+
self.class.default_options[:base_uri] = @url = HTTParty.normalize_base_uri(url)
|
50
|
+
|
51
|
+
@public_key_url = public_key_url
|
52
|
+
@key_size = key_size
|
53
|
+
@private_key_pem = private_key_pem
|
54
|
+
end
|
55
|
+
|
56
|
+
# POST request
|
57
|
+
#
|
58
|
+
# @todo Use DRY-schemas
|
59
|
+
# @param endpoint [String]
|
60
|
+
# @param body [Hash]
|
61
|
+
# @return [Hash]
|
62
|
+
def post(endpoint:, body:)
|
63
|
+
body = body.to_json
|
64
|
+
headers = default_headers
|
65
|
+
|
66
|
+
add_request_specific_headers! headers, 'post', endpoint
|
67
|
+
checksum_body! body, headers
|
68
|
+
sign_headers! headers
|
69
|
+
|
70
|
+
self.class.post(endpoint, body: body, headers: headers)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Loads or generates a private key
|
74
|
+
#
|
75
|
+
# @return [OpenSSL::PKey::RSA]
|
76
|
+
def private_key
|
77
|
+
@private_key ||= OpenSSL::PKey::RSA.new(@private_key_pem || key_size)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Public key
|
81
|
+
#
|
82
|
+
# @return [OpenSSL::PKey::RSA]
|
83
|
+
def public_key
|
84
|
+
private_key.public_key
|
85
|
+
end
|
86
|
+
|
87
|
+
# Host
|
88
|
+
#
|
89
|
+
# @return [String]
|
90
|
+
def host
|
91
|
+
@host ||= URI.parse(url).host
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def default_headers
|
97
|
+
{
|
98
|
+
'User-Agent' => "DistributedPress/#{DistributedPress::VERSION}",
|
99
|
+
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
100
|
+
'Host' => host
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
# HTTP Signatures
|
105
|
+
#
|
106
|
+
# @see {https://docs.joinmastodon.org/spec/security/}
|
107
|
+
# @param :headers [Hash]
|
108
|
+
def sign_headers!(headers)
|
109
|
+
headers['Signature'] = {
|
110
|
+
'keyId' => public_key_url,
|
111
|
+
'algorithm' => ALGORITHM,
|
112
|
+
'headers' => signable_headers(headers),
|
113
|
+
'signature' => signed_headers(headers)
|
114
|
+
}.map do |key, value|
|
115
|
+
"#{key}=\"#{value}\""
|
116
|
+
end.join(',')
|
117
|
+
|
118
|
+
headers.delete REQUEST_TARGET
|
119
|
+
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
|
123
|
+
# List of headers to be signed, removing headers that don't
|
124
|
+
# exist on the request.
|
125
|
+
#
|
126
|
+
# @return [String]
|
127
|
+
def signable_headers(headers)
|
128
|
+
(SIGNABLE_HEADERS & headers.keys).join(' ').downcase
|
129
|
+
end
|
130
|
+
|
131
|
+
# Sign headers
|
132
|
+
#
|
133
|
+
# @param :headers [Hash]
|
134
|
+
# @return [String]
|
135
|
+
def signed_headers(headers)
|
136
|
+
Base64.strict_encode64(
|
137
|
+
private_key.sign(
|
138
|
+
OpenSSL::Digest.new('SHA256'),
|
139
|
+
signature_content(headers)
|
140
|
+
)
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Generates a string to be signed
|
145
|
+
#
|
146
|
+
# @param :headers [Hash]
|
147
|
+
# @return [String]
|
148
|
+
def signature_content(headers)
|
149
|
+
headers.slice(*SIGNABLE_HEADERS).map do |key, value|
|
150
|
+
"#{key.downcase}: #{value}"
|
151
|
+
end.join("\n")
|
152
|
+
end
|
153
|
+
|
154
|
+
# Generate a checksum for the request body
|
155
|
+
#
|
156
|
+
# @param :body [String]
|
157
|
+
# @param :headers [Hash]
|
158
|
+
def checksum_body!(body, headers)
|
159
|
+
headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest body}"
|
160
|
+
|
161
|
+
nil
|
162
|
+
end
|
163
|
+
|
164
|
+
# Headers specific to a single request
|
165
|
+
#
|
166
|
+
# @param :headers [Hash] Headers
|
167
|
+
# @param :verb [String] HTTP verb
|
168
|
+
# @param :endpoint [String] Path
|
169
|
+
def add_request_specific_headers!(headers, verb, endpoint)
|
170
|
+
headers['Date'] = Time.now.utc.httpdate
|
171
|
+
headers[REQUEST_TARGET] = "#{verb} #{endpoint}"
|
172
|
+
|
173
|
+
nil
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
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.
|
4
|
+
version: 0.3.0rc0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- f
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-28 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:
|
314
|
+
version: 1.3.1
|
313
315
|
requirements: []
|
314
316
|
rubygems_version: 3.3.26
|
315
317
|
signing_key:
|