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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c9711c3976ab3430f3a9c4a2d22987fab3ff9ae28fc30a88e86df35957d8a9b
4
- data.tar.gz: bdfc5e017c2f264329f44eb87b2a8e4cc3e62586174b227a2297ba59fe4732c1
3
+ metadata.gz: bb2abf38d1d43c103dfd54f5bf1a77310c60e507a4c4c5f2b9bd7a314b8d1f64
4
+ data.tar.gz: a655bf162c53f6f5aed16dd51337927cf7c0e4c9858adca7803af0c83e7e5ab4
5
5
  SHA512:
6
- metadata.gz: 50af08ed6369f0b5280049af3cfc7ffb767f9d2f7d399657009dea3f79caba5716eba485097cb4e4cd2463113390cb607fc62fe175e641713d2ec36ed15f230e
7
- data.tar.gz: e3698a273dff671a1bc32848339d74f5ae079525cacf3c3196aaa0b899d225012a749e912f3b9bd7c3adc0e52c511e63f6173fdf87e5904bb4d321c8738cc273
6
+ metadata.gz: 2194448999c1d61ce195ebf09f7c1bb844b1d4e20c8e85b515bcfafc7d871fd644a8ced697522b6b0fc639164c818704b589ac9311570332147ee17549658a90
7
+ data.tar.gz: dbdf4e26219962c39f157b2d2d405cd3176c2b50b0a228a595686e80df5aff68c0477690c671a5677221eacef28766694617e87417c3157c98b82e3bf8959320
data/.rubocop.yml CHANGED
@@ -1,8 +1,8 @@
1
- require:
1
+ plugins:
2
2
  - rubocop-rspec
3
3
 
4
4
  AllCops:
5
- TargetRubyVersion: 3.2
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
- 3.2.2
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
- Ensure you have the required gems installed:
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`: Used for making HTTP requests.
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
- include HTTParty
9
- headers "Content-Type" => "application/json"
10
- default_options.update(verify: false)
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
- self.class.base_uri(api_url)
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("/access-keys")["accessKeys"]
31
+ request(:get, "/access-keys")["accessKeys"]
18
32
  end
19
33
 
20
34
  def transferred_data_by_id
21
- get("/metrics/transfer")
35
+ request(:get, "/metrics/transfer")
22
36
  end
23
37
 
24
38
  def create_key
25
- post("/access-keys")
39
+ request(:post, "/access-keys")
26
40
  end
27
41
 
28
42
  def set_limit(key_id, limit)
29
- body = { limit: { bytes: limit } }.to_json
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 = { name: }.to_json
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
- response = self.class.delete("/access-keys/#{key_id}")
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
- def get(endpoint)
46
- parse_response(self.class.get(endpoint))
47
- end
56
+ attr_reader :base_uri, :options
48
57
 
49
- def post(endpoint, body = {})
50
- parse_response(self.class.post(endpoint, body:))
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 put(endpoint, body, success_message)
54
- response = self.class.put(endpoint, body:)
55
- handle_response(response, success_message)
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 parse_response(response)
59
- JSON.parse(response.body)
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 handle_response(response, success_message)
63
- if response.code.to_i == 204
64
- puts(success_message)
65
- nil
66
- else
67
- parse_response(response)
68
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OutlineVpnApi
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -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
@@ -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.2.0"
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.2.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: 2023-10-27 00:00:00.000000000 Z
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: '0'
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: '0'
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.2.0
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: 3.4.10
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: []