outline_vpn_api 0.2.0 → 0.3.0
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 +4 -4
- data/.rubocop.yml +12 -2
- data/.ruby-version +1 -1
- data/README.md +43 -5
- data/lib/outline_vpn_api/client.rb +51 -31
- data/lib/outline_vpn_api/errors.rb +19 -0
- data/lib/outline_vpn_api/pinned_connection_adapter.rb +28 -0
- data/lib/outline_vpn_api/version.rb +1 -1
- data/lib/outline_vpn_api.rb +3 -2
- data/outline_vpn_api.gemspec +3 -3
- metadata +8 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bb2abf38d1d43c103dfd54f5bf1a77310c60e507a4c4c5f2b9bd7a314b8d1f64
|
|
4
|
+
data.tar.gz: a655bf162c53f6f5aed16dd51337927cf7c0e4c9858adca7803af0c83e7e5ab4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2194448999c1d61ce195ebf09f7c1bb844b1d4e20c8e85b515bcfafc7d871fd644a8ced697522b6b0fc639164c818704b589ac9311570332147ee17549658a90
|
|
7
|
+
data.tar.gz: dbdf4e26219962c39f157b2d2d405cd3176c2b50b0a228a595686e80df5aff68c0477690c671a5677221eacef28766694617e87417c3157c98b82e3bf8959320
|
data/.rubocop.yml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
plugins:
|
|
2
2
|
- rubocop-rspec
|
|
3
3
|
|
|
4
4
|
AllCops:
|
|
5
|
-
TargetRubyVersion: 3.
|
|
5
|
+
TargetRubyVersion: 3.4
|
|
6
6
|
NewCops: enable
|
|
7
7
|
SuggestExtensions: false
|
|
8
8
|
|
|
@@ -19,3 +19,13 @@ Style/StringLiteralsInInterpolation:
|
|
|
19
19
|
|
|
20
20
|
Layout/LineLength:
|
|
21
21
|
Max: 125
|
|
22
|
+
|
|
23
|
+
Metrics:
|
|
24
|
+
Exclude:
|
|
25
|
+
- "spec/**/*"
|
|
26
|
+
|
|
27
|
+
RSpec/ExampleLength:
|
|
28
|
+
Max: 10
|
|
29
|
+
|
|
30
|
+
RSpec/MultipleExpectations:
|
|
31
|
+
Max: 3
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
4.0.6
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@ It provides a simple interface to manage and retrieve information about VPN keys
|
|
|
5
5
|
|
|
6
6
|
## Installation
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Requires Ruby 3.4 or newer.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
gem install outline_vpn_api
|
|
@@ -21,6 +21,25 @@ To start using the client, initialize it with the API URL:
|
|
|
21
21
|
client = OutlineVpnApi.new('YOUR_API_URL')
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Outline servers present a self-signed certificate, so by default the client does
|
|
25
|
+
not verify it. Pass the `certSha256` fingerprint that Outline Manager shows next
|
|
26
|
+
to the API URL to pin the certificate instead:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
client = OutlineVpnApi.new('YOUR_API_URL', cert_sha256: 'AB:CD:...')
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
With a fingerprint set, a connection to any other certificate is rejected with
|
|
33
|
+
`OutlineVpnApi::CertificateError`. Without one the connection can be
|
|
34
|
+
intercepted, and since the API key is part of the URL an interceptor gains full
|
|
35
|
+
control of the server — pinning is strongly recommended.
|
|
36
|
+
|
|
37
|
+
Requests time out after 30 seconds by default:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
client = OutlineVpnApi.new('YOUR_API_URL', timeout: 10)
|
|
41
|
+
```
|
|
42
|
+
|
|
24
43
|
### Methods
|
|
25
44
|
|
|
26
45
|
#### `keys_list`
|
|
@@ -49,7 +68,7 @@ new_key = client.create_key
|
|
|
49
68
|
|
|
50
69
|
#### `set_limit(key_id, limit)`
|
|
51
70
|
|
|
52
|
-
Sets a data limit for a specific key
|
|
71
|
+
Sets a data limit for a specific key. Returns `true`:
|
|
53
72
|
|
|
54
73
|
```ruby
|
|
55
74
|
client.set_limit('KEY_ID', LIMIT_IN_BYTES)
|
|
@@ -57,7 +76,7 @@ client.set_limit('KEY_ID', LIMIT_IN_BYTES)
|
|
|
57
76
|
|
|
58
77
|
#### `rename_key(key_id, name)`
|
|
59
78
|
|
|
60
|
-
Renames a specific key
|
|
79
|
+
Renames a specific key. Returns `true`:
|
|
61
80
|
|
|
62
81
|
```ruby
|
|
63
82
|
client.rename_key('KEY_ID', 'NEW_NAME')
|
|
@@ -65,15 +84,34 @@ client.rename_key('KEY_ID', 'NEW_NAME')
|
|
|
65
84
|
|
|
66
85
|
#### `delete_key(key_id)`
|
|
67
86
|
|
|
68
|
-
Deletes a specific key
|
|
87
|
+
Deletes a specific key. Returns `true`:
|
|
69
88
|
|
|
70
89
|
```ruby
|
|
71
90
|
client.delete_key('KEY_ID')
|
|
72
91
|
```
|
|
73
92
|
|
|
93
|
+
## Errors
|
|
94
|
+
|
|
95
|
+
All errors inherit from `OutlineVpnApi::Error`:
|
|
96
|
+
|
|
97
|
+
- `OutlineVpnApi::ResponseError` — the server replied with a non-2xx status, or
|
|
98
|
+
with a body that is not valid JSON. Exposes `#code` and `#body`.
|
|
99
|
+
- `OutlineVpnApi::ConnectionError` — the server could not be reached, or the
|
|
100
|
+
request timed out.
|
|
101
|
+
- `OutlineVpnApi::CertificateError` — a pinned fingerprint was given and the
|
|
102
|
+
server's certificate did not match it. Subclass of `ConnectionError`.
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
begin
|
|
106
|
+
client.keys_list
|
|
107
|
+
rescue OutlineVpnApi::ResponseError => e
|
|
108
|
+
warn "Outline returned #{e.code}: #{e.body}"
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
74
112
|
## Dependencies
|
|
75
113
|
|
|
76
|
-
- `httparty
|
|
114
|
+
- `httparty` (>= 0.24.0): Used for making HTTP requests.
|
|
77
115
|
- `json`: Used for parsing JSON responses.
|
|
78
116
|
|
|
79
117
|
## Contributing
|
|
@@ -2,70 +2,90 @@
|
|
|
2
2
|
|
|
3
3
|
require "httparty"
|
|
4
4
|
require "json"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
require_relative "pinned_connection_adapter"
|
|
5
9
|
|
|
6
10
|
module OutlineVpnApi
|
|
7
11
|
class Client
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
DEFAULT_TIMEOUT = 30
|
|
13
|
+
|
|
14
|
+
CONNECTION_ERRORS = [
|
|
15
|
+
SocketError,
|
|
16
|
+
SystemCallError,
|
|
17
|
+
Net::OpenTimeout,
|
|
18
|
+
Net::ReadTimeout,
|
|
19
|
+
HTTParty::RedirectionTooDeep
|
|
20
|
+
].freeze
|
|
11
21
|
|
|
12
|
-
def initialize(api_url)
|
|
13
|
-
|
|
22
|
+
def initialize(api_url, cert_sha256: nil, timeout: DEFAULT_TIMEOUT)
|
|
23
|
+
@base_uri = api_url.to_s.chomp("/")
|
|
24
|
+
@options = {
|
|
25
|
+
headers: { "Content-Type" => "application/json" },
|
|
26
|
+
timeout: timeout
|
|
27
|
+
}.merge(ssl_options(cert_sha256))
|
|
14
28
|
end
|
|
15
29
|
|
|
16
30
|
def keys_list
|
|
17
|
-
get
|
|
31
|
+
request(:get, "/access-keys")["accessKeys"]
|
|
18
32
|
end
|
|
19
33
|
|
|
20
34
|
def transferred_data_by_id
|
|
21
|
-
get
|
|
35
|
+
request(:get, "/metrics/transfer")
|
|
22
36
|
end
|
|
23
37
|
|
|
24
38
|
def create_key
|
|
25
|
-
post
|
|
39
|
+
request(:post, "/access-keys")
|
|
26
40
|
end
|
|
27
41
|
|
|
28
42
|
def set_limit(key_id, limit)
|
|
29
|
-
body
|
|
30
|
-
put("/access-keys/#{key_id}/data-limit", body, "Limit for key_id: #{key_id} is #{limit} bytes")
|
|
43
|
+
request(:put, "/access-keys/#{key_id}/data-limit", body: { limit: { bytes: limit } })
|
|
31
44
|
end
|
|
32
45
|
|
|
33
46
|
def rename_key(key_id, name)
|
|
34
|
-
body
|
|
35
|
-
put("/access-keys/#{key_id}/name", body, "key_id: #{key_id}, renamed to #{name}")
|
|
47
|
+
request(:put, "/access-keys/#{key_id}/name", body: { name: })
|
|
36
48
|
end
|
|
37
49
|
|
|
38
50
|
def delete_key(key_id)
|
|
39
|
-
|
|
40
|
-
handle_response(response, "key_id: #{key_id}, deleted")
|
|
51
|
+
request(:delete, "/access-keys/#{key_id}")
|
|
41
52
|
end
|
|
42
53
|
|
|
43
54
|
private
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
parse_response(self.class.get(endpoint))
|
|
47
|
-
end
|
|
56
|
+
attr_reader :base_uri, :options
|
|
48
57
|
|
|
49
|
-
def
|
|
50
|
-
|
|
58
|
+
def request(verb, endpoint, body: nil)
|
|
59
|
+
request_options = body.nil? ? options : options.merge(body: body.to_json)
|
|
60
|
+
handle(HTTParty.public_send(verb, "#{base_uri}#{endpoint}", **request_options))
|
|
61
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
62
|
+
raise CertificateError, e.message
|
|
63
|
+
rescue *CONNECTION_ERRORS => e
|
|
64
|
+
raise ConnectionError, e.message
|
|
51
65
|
end
|
|
52
66
|
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
def handle(response)
|
|
68
|
+
code = response.code.to_i
|
|
69
|
+
raise ResponseError.new(code, response.body) unless code.between?(200, 299)
|
|
70
|
+
return true if response.body.nil? || response.body.empty?
|
|
71
|
+
|
|
72
|
+
parse(response.body, code)
|
|
56
73
|
end
|
|
57
74
|
|
|
58
|
-
def
|
|
59
|
-
JSON.parse(
|
|
75
|
+
def parse(body, code)
|
|
76
|
+
JSON.parse(body)
|
|
77
|
+
rescue JSON::ParserError
|
|
78
|
+
raise ResponseError.new(code, body, "Outline API returned malformed JSON (HTTP #{code})")
|
|
60
79
|
end
|
|
61
80
|
|
|
62
|
-
def
|
|
63
|
-
if
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
def ssl_options(cert_sha256)
|
|
82
|
+
return { verify: false } if cert_sha256.nil?
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
verify: true,
|
|
86
|
+
cert_sha256: cert_sha256,
|
|
87
|
+
connection_adapter: PinnedConnectionAdapter
|
|
88
|
+
}
|
|
69
89
|
end
|
|
70
90
|
end
|
|
71
91
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OutlineVpnApi
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConnectionError < Error; end
|
|
7
|
+
|
|
8
|
+
class CertificateError < ConnectionError; end
|
|
9
|
+
|
|
10
|
+
class ResponseError < Error
|
|
11
|
+
attr_reader :code, :body
|
|
12
|
+
|
|
13
|
+
def initialize(code, body, message = nil)
|
|
14
|
+
@code = code
|
|
15
|
+
@body = body
|
|
16
|
+
super(message || "Outline API returned HTTP #{code}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "httparty"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module OutlineVpnApi
|
|
7
|
+
class PinnedConnectionAdapter < HTTParty::ConnectionAdapter
|
|
8
|
+
def connection
|
|
9
|
+
http = super
|
|
10
|
+
return http unless http.use_ssl?
|
|
11
|
+
|
|
12
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
13
|
+
http.verify_hostname = false
|
|
14
|
+
http.verify_callback = ->(_preverify_ok, store_context) { pinned?(store_context.current_cert) }
|
|
15
|
+
http
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def pinned?(certificate)
|
|
21
|
+
OpenSSL::Digest::SHA256.hexdigest(certificate.to_der) == expected_fingerprint
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def expected_fingerprint
|
|
25
|
+
@expected_fingerprint ||= options.fetch(:cert_sha256).to_s.delete(":").downcase
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/outline_vpn_api.rb
CHANGED
|
@@ -4,10 +4,11 @@ require "httparty"
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
6
|
require_relative "outline_vpn_api/version"
|
|
7
|
+
require_relative "outline_vpn_api/errors"
|
|
7
8
|
require_relative "outline_vpn_api/client"
|
|
8
9
|
|
|
9
10
|
module OutlineVpnApi
|
|
10
|
-
def self.new(api_url)
|
|
11
|
-
OutlineVpnApi::Client.new(api_url)
|
|
11
|
+
def self.new(api_url, **)
|
|
12
|
+
OutlineVpnApi::Client.new(api_url, **)
|
|
12
13
|
end
|
|
13
14
|
end
|
data/outline_vpn_api.gemspec
CHANGED
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.description = "Ruby API wrapper for Outline VPN Server https://getoutline.org/"
|
|
13
13
|
spec.homepage = "https://github.com/vlasikhin/outline_vpn_api"
|
|
14
14
|
spec.license = "MIT"
|
|
15
|
-
spec.required_ruby_version = ">= 3.
|
|
15
|
+
spec.required_ruby_version = ">= 3.4.0"
|
|
16
16
|
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
18
|
spec.metadata["source_code_uri"] = spec.homepage
|
|
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
spec.files = Dir.chdir(__dir__) do
|
|
21
21
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
22
22
|
(File.expand_path(f) == __FILE__) || f.start_with?(
|
|
23
|
-
*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile]
|
|
23
|
+
*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile CLAUDE.md]
|
|
24
24
|
)
|
|
25
25
|
end
|
|
26
26
|
end
|
|
@@ -29,6 +29,6 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
30
30
|
spec.require_paths = ["lib"]
|
|
31
31
|
|
|
32
|
-
spec.add_dependency("httparty")
|
|
32
|
+
spec.add_dependency("httparty", ">= 0.24.0")
|
|
33
33
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
34
34
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: outline_vpn_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Pavel Vlasikhin
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: httparty
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
18
|
+
version: 0.24.0
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
25
|
+
version: 0.24.0
|
|
27
26
|
description: Ruby API wrapper for Outline VPN Server https://getoutline.org/
|
|
28
27
|
email:
|
|
29
28
|
- pavel.vlasikhin@gmail.com
|
|
@@ -40,6 +39,8 @@ files:
|
|
|
40
39
|
- Rakefile
|
|
41
40
|
- lib/outline_vpn_api.rb
|
|
42
41
|
- lib/outline_vpn_api/client.rb
|
|
42
|
+
- lib/outline_vpn_api/errors.rb
|
|
43
|
+
- lib/outline_vpn_api/pinned_connection_adapter.rb
|
|
43
44
|
- lib/outline_vpn_api/version.rb
|
|
44
45
|
- outline_vpn_api.gemspec
|
|
45
46
|
homepage: https://github.com/vlasikhin/outline_vpn_api
|
|
@@ -49,7 +50,6 @@ metadata:
|
|
|
49
50
|
homepage_uri: https://github.com/vlasikhin/outline_vpn_api
|
|
50
51
|
source_code_uri: https://github.com/vlasikhin/outline_vpn_api
|
|
51
52
|
rubygems_mfa_required: 'true'
|
|
52
|
-
post_install_message:
|
|
53
53
|
rdoc_options: []
|
|
54
54
|
require_paths:
|
|
55
55
|
- lib
|
|
@@ -57,15 +57,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
57
57
|
requirements:
|
|
58
58
|
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: 3.
|
|
60
|
+
version: 3.4.0
|
|
61
61
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
62
|
requirements:
|
|
63
63
|
- - ">="
|
|
64
64
|
- !ruby/object:Gem::Version
|
|
65
65
|
version: '0'
|
|
66
66
|
requirements: []
|
|
67
|
-
rubygems_version:
|
|
68
|
-
signing_key:
|
|
67
|
+
rubygems_version: 4.0.10
|
|
69
68
|
specification_version: 4
|
|
70
69
|
summary: Ruby API wrapper for Outline VPN Server
|
|
71
70
|
test_files: []
|