cineworld_uk 4.0.0 → 4.1.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/.github/workflows/test.yml +26 -0
- data/CHANGELOG.md +8 -0
- data/README.md +75 -6
- data/Rakefile +1 -0
- data/cineworld_uk.gemspec +1 -0
- data/lib/cineworld_uk/internal/guest_token.rb +73 -0
- data/lib/cineworld_uk/version.rb +2 -2
- data/lib/cineworld_uk.rb +6 -0
- data/tasks/package.rake +84 -0
- data/test/lib/cineworld_uk/internal/api_response_test.rb +76 -0
- data/test/package_test.rb +49 -0
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edfbb0e8f7d6238e878f4018a643befca62b662fffc47cad72367c2e404e9ee7
|
|
4
|
+
data.tar.gz: a0d59a9aa4d3227794fda2f78c00de9e59c62321b3cba3a50dbbd8611856cc82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aefe630b3ad344c2fbced398fc49c5fb9686387331f2cdea072f6dc96ef0893db42af90a6d277117cc7cd1b6ab4505b2fff3e64e859f79896503a71462934a89
|
|
7
|
+
data.tar.gz: cd3ad38fb9b9404504e5379ae03943424d56370b6ce2b0bcda069c9f64fa33a2802ce54dcb567740689408e882bda8e86cba3127193fb510f46fc73b2610c030
|
data/.github/workflows/test.yml
CHANGED
|
@@ -15,3 +15,29 @@ jobs:
|
|
|
15
15
|
ruby-version: ${{ matrix.ruby-version }}
|
|
16
16
|
bundler-cache: true
|
|
17
17
|
- run: bundle exec rake test
|
|
18
|
+
|
|
19
|
+
package:
|
|
20
|
+
strategy:
|
|
21
|
+
matrix:
|
|
22
|
+
include:
|
|
23
|
+
- runner: ubuntu-latest
|
|
24
|
+
platform: x86_64-linux-gnu
|
|
25
|
+
- runner: ubuntu-24.04-arm
|
|
26
|
+
platform: aarch64-linux-gnu
|
|
27
|
+
- runner: macos-15
|
|
28
|
+
platform: arm64-darwin
|
|
29
|
+
- runner: macos-15-intel
|
|
30
|
+
platform: x86_64-darwin
|
|
31
|
+
runs-on: ${{ matrix.runner }}
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
- uses: ruby/setup-ruby@v1
|
|
35
|
+
with:
|
|
36
|
+
ruby-version: '3.4'
|
|
37
|
+
bundler-cache: true
|
|
38
|
+
- run: bundle exec rake 'package:native[${{ matrix.platform }}]'
|
|
39
|
+
- run: CINEWORLD_PACKAGE="$(echo pkg/*.gem)" bundle exec ruby test/package_test.rb
|
|
40
|
+
- uses: actions/upload-artifact@v4
|
|
41
|
+
with:
|
|
42
|
+
name: gem-${{ matrix.platform }}
|
|
43
|
+
path: pkg/*.gem
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
All notable changes to this project will be documented in this file.
|
|
3
3
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
4
4
|
|
|
5
|
+
## 4.1.0 - 2026-10-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Explicit guest token acquisition and renewal with `CineworldUk.acquire_token!`, using the standalone `curl-impersonate` CLI without Python or a browser.
|
|
9
|
+
- Platform-specific gems bundle curl-impersonate 2.2.3 for x86-64/ARM64 glibc Linux and macOS, with pinned archive checksums, upstream notices, and native package CI checks. No separate executable installation is needed on these platforms.
|
|
10
|
+
- Optional executable path for token acquisition. Existing caller-supplied tokens continue to work without the CLI.
|
|
11
|
+
- Expiry checks and authentication errors for unavailable commands, blocked pages, and missing or invalid page tokens; failed acquisition preserves the previous token.
|
|
12
|
+
|
|
5
13
|
## 4.0.0 - 2026-10-15
|
|
6
14
|
|
|
7
15
|
### Changed
|
data/README.md
CHANGED
|
@@ -27,22 +27,70 @@ Performance titles are sanitized as much as possible, removing 'screening type'
|
|
|
27
27
|
|
|
28
28
|
### Authentication
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Acquire a guest JWT explicitly before retrieving data:
|
|
31
31
|
|
|
32
32
|
```ruby
|
|
33
33
|
require 'cineworld_uk'
|
|
34
34
|
|
|
35
|
+
CineworldUk.acquire_token!
|
|
36
|
+
CineworldUk::Cinema.all
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This shells out to **`curl-impersonate`**, not ordinary system `curl`, using its
|
|
40
|
+
Chrome 150 TLS/HTTP profile. No Python, Chrome, JavaScript execution or browser
|
|
41
|
+
cookies are needed. Platform-specific gem packages include the pinned
|
|
42
|
+
`curl-impersonate` 2.2.3 executable and its upstream licence notices. RubyGems
|
|
43
|
+
and Bundler automatically select the matching package for:
|
|
44
|
+
|
|
45
|
+
- glibc Linux: x86-64 and ARM64 (`x86_64-linux-gnu`, `aarch64-linux-gnu`)
|
|
46
|
+
- macOS: Intel and Apple Silicon (`x86_64-darwin`, `arm64-darwin`)
|
|
47
|
+
|
|
48
|
+
The executable runs directly from the installed gem. There are no separate
|
|
49
|
+
installation commands, PATH changes, or runtime downloads on these platforms.
|
|
50
|
+
|
|
51
|
+
When installing from Git, forcing the source (`ruby`) gem, or using another
|
|
52
|
+
platform (including Alpine/musl and Windows), install a compatible
|
|
53
|
+
[curl-impersonate release](https://github.com/lexiforest/curl-impersonate/releases/tag/v2.2.3)
|
|
54
|
+
with `--impersonate chrome150` support yourself. For an x86-64 Linux checkout:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
mkdir -p "$HOME/.local/bin"
|
|
58
|
+
curl -fL https://github.com/lexiforest/curl-impersonate/releases/download/v2.2.3/curl-impersonate-v2.2.3.x86_64-linux-gnu.tar.gz \
|
|
59
|
+
| tar -xz -C "$HOME/.local/bin" curl-impersonate
|
|
60
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Other platforms have separate assets on the release page. You can also specify
|
|
64
|
+
the executable path:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
CineworldUk.acquire_token!(curl: '/opt/curl-impersonate/curl-impersonate')
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The method returns the token and sets `CineworldUk.token` in memory. It reads
|
|
71
|
+
`__NEXT_DATA__.props.pageProps.environment.gasToken` from the ticketing site's
|
|
72
|
+
root page, even when the page displays a 404. It checks the declared expiry,
|
|
73
|
+
but decoding the JWT does not verify its signature. A blocked page, expired or
|
|
74
|
+
missing token, missing executable, or failed command raises
|
|
75
|
+
`CineworldUk::AuthenticationError` and leaves any previous token unchanged.
|
|
76
|
+
Requests time out after 30 seconds. Certificate verification remains enabled;
|
|
77
|
+
redirects are not followed, and subprocess arguments are passed without a shell.
|
|
78
|
+
Cloudflare may still block requests on other networks or after website changes.
|
|
79
|
+
|
|
80
|
+
Alternatively, supply your own current guest JWT without the `Bearer ` prefix;
|
|
81
|
+
this requires no external executable:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
35
84
|
CineworldUk.token = ENV.fetch('CINEWORLD_TOKEN')
|
|
36
85
|
```
|
|
37
86
|
|
|
38
87
|
The gem sends `Authorization: Bearer ...` and `Connect-Region-Code: GB` to
|
|
39
|
-
`digital-api.cineworld.co.uk`.
|
|
40
|
-
|
|
41
|
-
`__NEXT_DATA__.props.pageProps.environment.gasToken`; access to that page may
|
|
42
|
-
require an interactive browser.
|
|
88
|
+
`digital-api.cineworld.co.uk`. Normal API calls never acquire or renew a token
|
|
89
|
+
automatically.
|
|
43
90
|
|
|
44
91
|
Missing tokens and HTTP 401 responses raise `CineworldUk::AuthenticationError`.
|
|
45
|
-
|
|
92
|
+
Call `CineworldUk.acquire_token!` again, or set `CineworldUk.token` with a fresh
|
|
93
|
+
token, before retrying. Tokens are never persisted by the gem. Other HTTP
|
|
46
94
|
failures raise an error rather than returning an empty schedule. Redirects are
|
|
47
95
|
not followed, so credentials are not forwarded to another host.
|
|
48
96
|
|
|
@@ -145,6 +193,27 @@ performance.cinema_id
|
|
|
145
193
|
#=> 14
|
|
146
194
|
```
|
|
147
195
|
|
|
196
|
+
## Building release packages
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
bundle exec rake build # Portable source gem; external curl-impersonate required
|
|
200
|
+
bundle exec rake package:all # Four platform gems with bundled executables
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
To build and check just one platform on a matching host:
|
|
204
|
+
|
|
205
|
+
```sh
|
|
206
|
+
bundle exec rake 'package:native[x86_64-linux-gnu]'
|
|
207
|
+
CINEWORLD_PACKAGE=pkg/cineworld_uk-4.1.0-x86_64-linux-gnu.gem bundle exec ruby test/package_test.rb
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Native packaging downloads pinned upstream archives at **build time**, verifies
|
|
211
|
+
their SHA-256 digests, and includes the executable, licence files and source
|
|
212
|
+
provenance. Archives and built gems stay in ignored `pkg/`, not Git. CI builds
|
|
213
|
+
and installation-tests each platform on its corresponding runner and saves the
|
|
214
|
+
gems as workflow artifacts; it does not publish them. Publish the source and
|
|
215
|
+
platform gems together for automatic platform selection to work for users.
|
|
216
|
+
|
|
148
217
|
## Contributing
|
|
149
218
|
|
|
150
219
|
1. Fork it
|
data/Rakefile
CHANGED
data/cineworld_uk.gemspec
CHANGED
|
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
spec.add_development_dependency 'simplecov', '< 1'
|
|
25
25
|
spec.add_development_dependency 'webmock'
|
|
26
26
|
|
|
27
|
+
spec.add_runtime_dependency 'base64', '~> 0.3'
|
|
27
28
|
spec.add_runtime_dependency 'cinebase', '~> 5.0'
|
|
28
29
|
spec.add_runtime_dependency 'nokogiri', '>= 1.11'
|
|
29
30
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
require "nokogiri"
|
|
6
|
+
require "open3"
|
|
7
|
+
|
|
8
|
+
module CineworldUk
|
|
9
|
+
module Internal
|
|
10
|
+
# Retrieves the public guest token without running a browser.
|
|
11
|
+
class GuestToken
|
|
12
|
+
def initialize(curl:)
|
|
13
|
+
@curl = curl || default_curl
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fetch
|
|
17
|
+
token = extract_token(request_page)
|
|
18
|
+
validate_expiry!(token)
|
|
19
|
+
token
|
|
20
|
+
rescue JSON::ParserError, ArgumentError, IndexError, TypeError, NoMethodError
|
|
21
|
+
raise AuthenticationError, "Guest token page data is missing or invalid", cause: nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def default_curl
|
|
27
|
+
bundled = File.expand_path("../../../vendor/curl-impersonate/bin/curl-impersonate", __dir__)
|
|
28
|
+
File.executable?(bundled) ? bundled : "curl-impersonate"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def curl_command
|
|
32
|
+
[@curl, "--disable", "--impersonate", "chrome150",
|
|
33
|
+
"--compressed", "--silent", "--show-error",
|
|
34
|
+
"--connect-timeout", "10", "--max-time", "30",
|
|
35
|
+
"--write-out", "\n%{http_code}", "https://web.cineworld.co.uk/"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def request_page
|
|
39
|
+
output, _stderr, result = Open3.capture3(*curl_command)
|
|
40
|
+
unless result.success?
|
|
41
|
+
raise AuthenticationError,
|
|
42
|
+
"Guest token request failed (curl exit #{result.exitstatus})"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
html, _, status = output.rpartition("\n")
|
|
46
|
+
unless %w[200 404].include?(status)
|
|
47
|
+
raise AuthenticationError, "Guest token page returned HTTP #{status}"
|
|
48
|
+
end
|
|
49
|
+
html
|
|
50
|
+
rescue Errno::ENOENT
|
|
51
|
+
raise AuthenticationError,
|
|
52
|
+
"Install curl-impersonate or pass its executable path with curl:"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def extract_token(html)
|
|
56
|
+
script = Nokogiri::HTML(html).at_css("script#__NEXT_DATA__")
|
|
57
|
+
data = JSON.parse(script ? script.content : "")
|
|
58
|
+
token = data.dig("props", "pageProps", "environment", "gasToken")
|
|
59
|
+
unless token.is_a?(String) && !token.empty?
|
|
60
|
+
raise AuthenticationError, "Guest token is missing from the page"
|
|
61
|
+
end
|
|
62
|
+
token
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_expiry!(token)
|
|
66
|
+
claims = JSON.parse(Base64.urlsafe_decode64(token.split(".").fetch(1)))
|
|
67
|
+
if Integer(claims.fetch("exp")) <= Time.now.to_i
|
|
68
|
+
raise AuthenticationError, "Guest token supplied by the page has expired"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/cineworld_uk/version.rb
CHANGED
data/lib/cineworld_uk.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'cinebase'
|
|
|
5
5
|
require_relative './cineworld_uk/version'
|
|
6
6
|
|
|
7
7
|
require_relative './cineworld_uk/internal/api_response'
|
|
8
|
+
require_relative './cineworld_uk/internal/guest_token'
|
|
8
9
|
require_relative './cineworld_uk/internal/parser/api/cinema_address'
|
|
9
10
|
require_relative './cineworld_uk/internal/parser/api/film'
|
|
10
11
|
require_relative './cineworld_uk/internal/parser/api/performance'
|
|
@@ -21,5 +22,10 @@ module CineworldUk
|
|
|
21
22
|
# A raw guest JWT, without the "Bearer " prefix. Never persisted by the gem.
|
|
22
23
|
class << self
|
|
23
24
|
attr_accessor :token
|
|
25
|
+
|
|
26
|
+
# Explicit acquisition/renewal; ordinary API calls never launch a subprocess.
|
|
27
|
+
def acquire_token!(curl: nil)
|
|
28
|
+
self.token = Internal::GuestToken.new(curl: curl).fetch
|
|
29
|
+
end
|
|
24
30
|
end
|
|
25
31
|
end
|
data/tasks/package.rake
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "rubygems/package"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "zlib"
|
|
8
|
+
|
|
9
|
+
namespace :package do
|
|
10
|
+
# SHA-256 digests from the upstream v2.2.3 release assets.
|
|
11
|
+
releases = {
|
|
12
|
+
"x86_64-linux-gnu" => ["x86_64-linux-gnu", "ca7c8aae49e33260d3a2400db16c7737aaf04692d606a843502056b42990f989"],
|
|
13
|
+
"aarch64-linux-gnu" => ["aarch64-linux-gnu", "5e71795b971f32be8f3d7785895f4dceb232f04414f5a067db266432a5b08657"],
|
|
14
|
+
"arm64-darwin" => ["arm64-macos", "2569f4139460fcb301484d37938de91b1c220efd15487ee22a9db554262062fe"],
|
|
15
|
+
"x86_64-darwin" => ["x86_64-macos", "4686806d59abea93866a917c3025a049ef3d7a3240de742ad66bd1a4d9890dc7"]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
desc "Build a gem with bundled curl-impersonate 2.2.3 for the given platform"
|
|
19
|
+
task :native, [:platform] do |_task, args|
|
|
20
|
+
platform = args.fetch(:platform)
|
|
21
|
+
target, digest = releases.fetch(platform)
|
|
22
|
+
root = File.expand_path("..", __dir__)
|
|
23
|
+
asset = "curl-impersonate-v2.2.3.#{target}.tar.gz"
|
|
24
|
+
url = "https://github.com/lexiforest/curl-impersonate/releases/download/v2.2.3/#{asset}"
|
|
25
|
+
archive = File.join(root, "pkg", "downloads", asset)
|
|
26
|
+
FileUtils.mkdir_p(File.dirname(archive))
|
|
27
|
+
unless File.exist?(archive)
|
|
28
|
+
sh "curl", "--fail", "--location", "--retry", "3", "--output", archive, url
|
|
29
|
+
end
|
|
30
|
+
abort "SHA-256 mismatch for #{archive}; delete it and retry" unless Digest::SHA256.file(archive).hexdigest == digest
|
|
31
|
+
|
|
32
|
+
spec = Gem::Specification.load(File.join(root, "cineworld_uk.gemspec")).dup
|
|
33
|
+
Dir.mktmpdir("cineworld-package") do |stage|
|
|
34
|
+
spec.files.each do |file|
|
|
35
|
+
destination = File.join(stage, file)
|
|
36
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
37
|
+
FileUtils.cp(File.join(root, file), destination)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
vendor = File.join(stage, "vendor", "curl-impersonate")
|
|
41
|
+
Zlib::GzipReader.open(archive) do |gzip|
|
|
42
|
+
Gem::Package::TarReader.new(gzip) do |tar|
|
|
43
|
+
tar.each do |entry|
|
|
44
|
+
next unless entry.file?
|
|
45
|
+
|
|
46
|
+
name = entry.full_name
|
|
47
|
+
relative = if name == "curl-impersonate"
|
|
48
|
+
"bin/curl-impersonate"
|
|
49
|
+
elsif name.start_with?("LICENSE") && File.basename(name) == name
|
|
50
|
+
"licenses/#{name}"
|
|
51
|
+
end
|
|
52
|
+
next unless relative
|
|
53
|
+
|
|
54
|
+
destination = File.join(vendor, relative)
|
|
55
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
56
|
+
File.binwrite(destination, entry.read)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
File.chmod(0o755, File.join(vendor, "bin", "curl-impersonate"))
|
|
61
|
+
File.write(File.join(vendor, "SOURCE"), <<~SOURCE)
|
|
62
|
+
curl-impersonate 2.2.3
|
|
63
|
+
Binary: #{url}
|
|
64
|
+
SHA-256: #{digest}
|
|
65
|
+
Source and build instructions: https://github.com/lexiforest/curl-impersonate/tree/v2.2.3
|
|
66
|
+
Redistribution notices: licenses/
|
|
67
|
+
SOURCE
|
|
68
|
+
|
|
69
|
+
Dir.chdir(stage) do
|
|
70
|
+
spec.platform = Gem::Platform.new(platform)
|
|
71
|
+
spec.files += Dir.glob("vendor/**/*").select { |file| File.file?(file) }
|
|
72
|
+
Gem::Package.build(spec, false, false, File.join(root, "pkg", spec.file_name))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
desc "Build all supported platform gems with bundled curl-impersonate"
|
|
78
|
+
task :all do
|
|
79
|
+
releases.each_key do |platform|
|
|
80
|
+
Rake::Task["package:native"].reenable
|
|
81
|
+
Rake::Task["package:native"].invoke(platform)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../../test_helper"
|
|
4
|
+
require "tmpdir"
|
|
4
5
|
|
|
5
6
|
# This block groups independent Minitest cases, not application logic.
|
|
6
7
|
describe "Vista retrieval" do # rubocop:disable Metrics/BlockLength
|
|
@@ -129,3 +130,78 @@ describe "Vista retrieval" do # rubocop:disable Metrics/BlockLength
|
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
end
|
|
133
|
+
|
|
134
|
+
describe "Guest token acquisition" do
|
|
135
|
+
before do
|
|
136
|
+
@directory = Dir.mktmpdir("cineworld token ")
|
|
137
|
+
@curl = File.join(@directory, "curl-impersonate")
|
|
138
|
+
CineworldUk.token = "existing-token"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
after do
|
|
142
|
+
FileUtils.remove_entry(@directory)
|
|
143
|
+
CineworldUk.token = nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def serve_token_page(html, status: 200, exit_code: 0)
|
|
147
|
+
output = "#{html}\n#{status}"
|
|
148
|
+
File.write(@curl, "#!#{RbConfig.ruby}\nSTDOUT.write(#{output.dump})\nexit #{exit_code}\n")
|
|
149
|
+
File.chmod(0o700, @curl)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def token_page(expires)
|
|
153
|
+
payload = Base64.urlsafe_encode64(JSON.generate("exp" => expires), padding: false)
|
|
154
|
+
token = "header.#{payload}.signature"
|
|
155
|
+
data = { "props" => { "pageProps" => { "environment" => { "gasToken" => token } } } }
|
|
156
|
+
["<script type='application/json' id='__NEXT_DATA__'>#{JSON.generate(data)}</script>", token]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "sets and returns the guest JWT from either a 200 or 404 page" do
|
|
160
|
+
html, token = token_page(Time.now.to_i + 3600)
|
|
161
|
+
[200, 404].each do |status|
|
|
162
|
+
serve_token_page(html, status: status)
|
|
163
|
+
_(CineworldUk.acquire_token!(curl: @curl)).must_equal token
|
|
164
|
+
_(CineworldUk.token).must_equal token
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "rejects expired tokens including the expiry boundary without replacing the current token" do
|
|
169
|
+
now = Time.at(1_800_000_000)
|
|
170
|
+
Time.stub(:now, now) do
|
|
171
|
+
[-1, 0, 1].each do |offset|
|
|
172
|
+
html, token = token_page(now.to_i + offset)
|
|
173
|
+
serve_token_page(html)
|
|
174
|
+
if offset.positive?
|
|
175
|
+
_(CineworldUk.acquire_token!(curl: @curl)).must_equal token
|
|
176
|
+
else
|
|
177
|
+
assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
|
|
178
|
+
_(CineworldUk.token).must_equal "existing-token"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "reports blocked, redirected, missing and malformed page data without exposing the response" do
|
|
185
|
+
html, token = token_page(Time.now.to_i + 3600)
|
|
186
|
+
invalid_payload = Base64.urlsafe_encode64("broken-secret", padding: false)
|
|
187
|
+
[[html, 403], [html, 302], ["Cloudflare block", 200],
|
|
188
|
+
["<script id='__NEXT_DATA__'>{</script>", 200],
|
|
189
|
+
["<script id='__NEXT_DATA__'>{}</script>", 200],
|
|
190
|
+
[html.sub(token, "broken-secret"), 200],
|
|
191
|
+
[html.sub(token, "header.#{invalid_payload}.signature"), 200]].each do |body, status|
|
|
192
|
+
serve_token_page(body, status: status)
|
|
193
|
+
error = assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
|
|
194
|
+
refute_includes error.full_message, token
|
|
195
|
+
refute_includes error.full_message, "broken-secret"
|
|
196
|
+
_(CineworldUk.token).must_equal "existing-token"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "reports a missing executable or curl failure as an authentication error" do
|
|
201
|
+
error = assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
|
|
202
|
+
assert_includes error.message, "Install curl-impersonate"
|
|
203
|
+
serve_token_page("", status: 0, exit_code: 28)
|
|
204
|
+
error = assert_raises(CineworldUk::AuthenticationError) { CineworldUk.acquire_token!(curl: @curl) }
|
|
205
|
+
assert_includes error.message, "curl exit 28"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Run separately after building: CINEWORLD_PACKAGE=pkg/...gem bundle exec ruby test/package_test.rb
|
|
2
|
+
require "minitest/autorun"
|
|
3
|
+
require "rubygems/installer"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "base64"
|
|
8
|
+
require "json"
|
|
9
|
+
|
|
10
|
+
describe "Platform package" do
|
|
11
|
+
before do
|
|
12
|
+
@directory = Dir.mktmpdir("cineworld-package-test")
|
|
13
|
+
@spec = Gem::Installer.at(
|
|
14
|
+
ENV.fetch("CINEWORLD_PACKAGE"), install_dir: @directory, ignore_dependencies: true
|
|
15
|
+
).install
|
|
16
|
+
@vendor = File.join(@spec.full_gem_path, "vendor", "curl-impersonate")
|
|
17
|
+
@binary = File.join(@vendor, "bin", "curl-impersonate")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
after do
|
|
21
|
+
FileUtils.remove_entry(@directory)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "installs a runnable executable with upstream notices and provenance" do
|
|
25
|
+
assert File.executable?(@binary)
|
|
26
|
+
output, status = Open3.capture2(@binary, "--version")
|
|
27
|
+
assert status.success?
|
|
28
|
+
assert_includes output, "curl"
|
|
29
|
+
assert File.file?(File.join(@vendor, "licenses", "LICENSE_CURL"))
|
|
30
|
+
assert_includes File.read(File.join(@vendor, "SOURCE")), "2.2.3"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "acquires a token using the gem-local executable without any curl on PATH" do
|
|
34
|
+
payload = Base64.urlsafe_encode64(JSON.generate("exp" => Time.now.to_i + 3600), padding: false)
|
|
35
|
+
token = "header.#{payload}.signature"
|
|
36
|
+
data = { "props" => { "pageProps" => { "environment" => { "gasToken" => token } } } }
|
|
37
|
+
page = "<script id='__NEXT_DATA__'>#{JSON.generate(data)}</script>\n200"
|
|
38
|
+
# Substitute only the network response, not the installed package's path resolution.
|
|
39
|
+
File.write(@binary, "#!#{RbConfig.ruby}\nSTDOUT.write(#{page.dump})\n")
|
|
40
|
+
output, error, status = Open3.capture3(
|
|
41
|
+
{ "PATH" => @directory, "RUBYOPT" => nil, "BUNDLER_SETUP" => nil,
|
|
42
|
+
"GEM_PATH" => Gem.path.join(File::PATH_SEPARATOR) }, RbConfig.ruby,
|
|
43
|
+
"-I#{File.join(@spec.full_gem_path, 'lib')}", "-rcineworld_uk",
|
|
44
|
+
"-e", "print CineworldUk.acquire_token!"
|
|
45
|
+
)
|
|
46
|
+
assert status.success?, error
|
|
47
|
+
_(output).must_equal token
|
|
48
|
+
end
|
|
49
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cineworld_uk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Croll
|
|
@@ -93,6 +93,20 @@ dependencies:
|
|
|
93
93
|
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: base64
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0.3'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0.3'
|
|
96
110
|
- !ruby/object:Gem::Dependency
|
|
97
111
|
name: cinebase
|
|
98
112
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -142,12 +156,14 @@ files:
|
|
|
142
156
|
- lib/cineworld_uk.rb
|
|
143
157
|
- lib/cineworld_uk/cinema.rb
|
|
144
158
|
- lib/cineworld_uk/internal/api_response.rb
|
|
159
|
+
- lib/cineworld_uk/internal/guest_token.rb
|
|
145
160
|
- lib/cineworld_uk/internal/parser/api/cinema_address.rb
|
|
146
161
|
- lib/cineworld_uk/internal/parser/api/film.rb
|
|
147
162
|
- lib/cineworld_uk/internal/parser/api/performance.rb
|
|
148
163
|
- lib/cineworld_uk/internal/title_sanitizer.rb
|
|
149
164
|
- lib/cineworld_uk/performance.rb
|
|
150
165
|
- lib/cineworld_uk/version.rb
|
|
166
|
+
- tasks/package.rake
|
|
151
167
|
- test/fixtures/api/cinema-list.json
|
|
152
168
|
- test/fixtures/api/dates-14.json
|
|
153
169
|
- test/fixtures/api/performances-tomorrow-14.json
|
|
@@ -161,6 +177,7 @@ files:
|
|
|
161
177
|
- test/lib/cineworld_uk/performance_test.rb
|
|
162
178
|
- test/lib/cineworld_uk/version_test.rb
|
|
163
179
|
- test/live/integration_test.rb
|
|
180
|
+
- test/package_test.rb
|
|
164
181
|
- test/support/fixture_reader.rb
|
|
165
182
|
- test/test_helper.rb
|
|
166
183
|
homepage: https://github.com/andycroll/cineworld_uk
|
|
@@ -199,5 +216,6 @@ test_files:
|
|
|
199
216
|
- test/lib/cineworld_uk/performance_test.rb
|
|
200
217
|
- test/lib/cineworld_uk/version_test.rb
|
|
201
218
|
- test/live/integration_test.rb
|
|
219
|
+
- test/package_test.rb
|
|
202
220
|
- test/support/fixture_reader.rb
|
|
203
221
|
- test/test_helper.rb
|