fastlane-plugin-charles 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fbb9bce2869ecdce4ed25d8d4c8566910c4842d841c3699f41fcbfbdd5d0550e
4
+ data.tar.gz: 66d7e3dd9a1de505b3ef8b3951267dacfe875f3e6a21c98131ea58039a301d51
5
+ SHA512:
6
+ metadata.gz: c2448b5eb66dca95d838b146dfb3c4eb8521a105311c1a3f496d70413539b473906d090a6656773068f3a322dd972b373be789b429366484ad3c387dc6ab5ccb
7
+ data.tar.gz: 5f32e89226f22db3235589752212427a89d90c7a34fb434ce43b8c141249ed9a27103fb0e4e7e639df7ed4e2f4347732cf4c213efd494dfc964dcbb365f591d9
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-06-30
11
+
12
+ ### Added
13
+
14
+ - `charles` action: launches Charles Proxy on macOS, generating its `.config`
15
+ XML at runtime from a simplified `charles.yml` (see
16
+ [`example/charles.yml`](example/charles.yml)) instead of requiring
17
+ developers to hand-edit and commit Charles's own exported XML.
18
+ - `proxy.enable_socks` and `proxy.ssl` for controlling Charles's SOCKS proxy
19
+ and SSL decryption, including per-host port overrides and an
20
+ `include`/`exclude` form for carving hosts out of SSL decryption.
21
+ - `recording.hosts`, using a generalized location matcher
22
+ (`protocol`/`host`/`port`/`path`/`query`, each optional) shared with future
23
+ location-based config.
24
+ - `access_control.ip_ranges` in `charles.yml` (with an `"all"` shorthand for
25
+ `0.0.0.0/0`), merged at runtime with the per-developer `ip_ranges` action
26
+ option — CIDR notation or bare IPs (treated as `/32`).
27
+ - `registered_name` / `registered_key` action options for Charles Proxy
28
+ license registration, sourced from env vars rather than committed config.
29
+ - Automatic EULA acceptance in the generated config.
30
+ - Clear, actionable errors (instead of raw Ruby exceptions) for common
31
+ `charles.yml` mistakes: a missing file, invalid YAML syntax, YAML that
32
+ isn't a mapping at the top level, and malformed `ip_ranges` CIDR entries.
33
+
34
+ ### Known limitations
35
+
36
+ - Charles's `toolConfiguration` tools (Breakpoints, Rewrite, Map Remote,
37
+ Block/Allow List, etc.) are not yet supported — see
38
+ [`docs/tool-configuration.md`](docs/tool-configuration.md) for the
39
+ per-tool evaluation and rationale.
40
+
41
+ [Unreleased]: https://github.com/jakekrog/fastlane-plugin-charles/compare/v0.1.0...HEAD
42
+ [0.1.0]: https://github.com/jakekrog/fastlane-plugin-charles/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Jake Krog (https://github.com/jakekrog)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # Charles `fastlane` Plugin
2
+
3
+ [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-charles)
4
+ [![Gem Version](https://img.shields.io/gem/v/fastlane-plugin-charles.svg)](https://rubygems.org/gems/fastlane-plugin-charles)
5
+ [![Test](https://github.com/jakekrog/fastlane-plugin-charles/actions/workflows/test.yml/badge.svg)](https://github.com/jakekrog/fastlane-plugin-charles/actions/workflows/test.yml)
6
+ [![pre-commit](https://github.com/jakekrog/fastlane-plugin-charles/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/jakekrog/fastlane-plugin-charles/actions/workflows/pre-commit.yml)
7
+ [![License: MIT](https://img.shields.io/github/license/jakekrog/fastlane-plugin-charles.svg)](LICENSE)
8
+
9
+ ## Getting Started
10
+
11
+ This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To get started with `fastlane-plugin-charles`, add it to your project by running:
12
+
13
+ ```bash
14
+ fastlane add_plugin charles
15
+ ```
16
+
17
+ ## About charles
18
+
19
+ Run [Charles Proxy](https://www.charlesproxy.com/) from a fastlane lane, generating its `.config` file at runtime from a simplified YAML config (see [`example/charles.yml`](example/charles.yml)) that a team can commit and share, instead of hand-editing (and committing) Charles's own exported XML.
20
+
21
+ Currently macOS only (the default `app_path` points at the standard macOS Charles install).
22
+
23
+ ### Options
24
+
25
+ | Key | Env var | Description | Default |
26
+ | ----- | --------- | -------------- | --------- |
27
+ | `app_path` | `FL_CHARLES_APP_PATH` | Path to the Charles application executable | `/Applications/Charles.app/Contents/MacOS/Charles` |
28
+ | `config_path` | `FL_CHARLES_CONFIG_PATH` | Path to a simplified Charles YAML config (see [`example/charles.yml`](example/charles.yml)) | `charles.yml` |
29
+ | `registered_name` | `FL_CHARLES_REGISTERED_NAME` | Registered name for your Charles Proxy license | none |
30
+ | `registered_key` | `FL_CHARLES_REGISTERED_KEY` | License key for your Charles Proxy registration | none |
31
+ | `ip_ranges` | `FL_CHARLES_IP_RANGES` | Per-developer IP ranges permitted to access the proxy | `[]` |
32
+
33
+ `registered_name` and `registered_key` are intentionally kept out of `charles.yml` since they're per-developer secrets, not shared team config — set them via env vars (or a `.env` file fastlane will load) instead of committing them. They must be provided together.
34
+
35
+ `ip_ranges` works as a hybrid: the `ip_ranges` action option/env var is for ranges that only apply to one developer (e.g. their own machine), while ranges that genuinely apply to the whole team (a shared corporate subnet, or `"all"` as shorthand for `0.0.0.0/0`) belong in `charles.yml`'s `access_control.ip_ranges` instead — the two lists are merged at runtime. Each entry can be full CIDR notation (`10.0.1.20/32`) or a bare IP (`10.0.1.20`), which is treated as `/32` (that single host).
36
+
37
+ See [`example/.env.example`](example/.env.example) for a template covering these env vars — copy it to `.env` (which stays gitignored) rather than committing real values.
38
+
39
+ ```ruby
40
+ charles # Use default paths
41
+ charles(app_path: "/path/to/Charles.app/Contents/MacOS/Charles")
42
+ charles(config_path: "/path/to/charles.yml")
43
+ charles(app_path: "/custom/path/to/Charles", config_path: "/custom/path/to/charles.yml")
44
+ ```
45
+
46
+ ## Example
47
+
48
+ Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use this plugin. Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`.
49
+
50
+ [`example/charles.yml`](example/charles.yml) shows the simplified schema this plugin generates a Charles `.config` file from at runtime.
51
+
52
+ `charles.yml` doesn't yet cover Charles's `toolConfiguration` tools (Breakpoints, Rewrite, Map Remote, Block List, etc.) — see [`docs/tool-configuration.md`](docs/tool-configuration.md) for the per-tool evaluation and why that's deferred to a future release.
53
+
54
+ ## Run tests for this plugin
55
+
56
+ To run both the tests, and code style validation, run
57
+
58
+ ```bash
59
+ bundle exec rake
60
+ ```
61
+
62
+ To automatically fix many of the styling issues, use
63
+
64
+ ```bash
65
+ bundle exec rubocop -a
66
+ ```
67
+
68
+ ## Issues and Feedback
69
+
70
+ For any other issues and feedback about this plugin, please submit it to this repository. See [CONTRIBUTING.md](CONTRIBUTING.md) if you'd like to open a PR, [CHANGELOG.md](CHANGELOG.md) for release history, [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for community guidelines, and [SECURITY.md](SECURITY.md) to report a vulnerability.
71
+
72
+ ## Troubleshooting
73
+
74
+ If you have trouble using plugins, check out the [Plugins Troubleshooting](https://docs.fastlane.tools/plugins/plugins-troubleshooting/) guide.
75
+
76
+ ## Using _fastlane_ Plugins
77
+
78
+ For more information about how the `fastlane` plugin system works, check out the [Plugins documentation](https://docs.fastlane.tools/plugins/create-plugin/).
79
+
80
+ ## About _fastlane_
81
+
82
+ _fastlane_ is the easiest way to automate beta deployments and releases for your iOS and Android apps. To learn more, check out [fastlane.tools](https://fastlane.tools).
@@ -0,0 +1,104 @@
1
+ require 'fastlane/action'
2
+ require 'fastlane_core/configuration/config_item'
3
+ require 'tmpdir'
4
+ require_relative '../helper/charles_helper'
5
+
6
+ module Fastlane
7
+ module Actions
8
+ class CharlesAction < Action
9
+ def self.run(params)
10
+ charles_app_path = params[:app_path]
11
+ yaml_config_path = params[:config_path]
12
+ config_xml = Helper::CharlesHelper.generate_config_xml(
13
+ yaml_config_path,
14
+ registered_name: params[:registered_name],
15
+ registered_key: params[:registered_key],
16
+ ip_ranges: params[:ip_ranges]
17
+ )
18
+
19
+ Dir.mktmpdir('fastlane-charles-') do |tmp_dir|
20
+ charles_config_path = File.join(tmp_dir, 'charles.config')
21
+ File.write(charles_config_path, config_xml)
22
+
23
+ Actions.sh(charles_app_path, '-config', charles_config_path)
24
+ end
25
+ end
26
+
27
+ def self.description
28
+ 'Run Charles HTTP proxy'
29
+ end
30
+
31
+ def self.authors
32
+ ['jakekrog']
33
+ end
34
+
35
+ def self.return_value
36
+ end
37
+
38
+ def self.details
39
+ 'This action starts Charles Proxy, generating its .config file at runtime from a simplified YAML config (see example/charles.yml). You can specify the path to the Charles application and the YAML config either through command-line arguments or environment variables.'
40
+ end
41
+
42
+ def self.available_options
43
+ [
44
+ FastlaneCore::ConfigItem.new(
45
+ key: :app_path,
46
+ env_name: 'FL_CHARLES_APP_PATH',
47
+ description: 'Path to Charles application executable',
48
+ default_value: '/Applications/Charles.app/Contents/MacOS/Charles',
49
+ optional: true,
50
+ type: String
51
+ ),
52
+ FastlaneCore::ConfigItem.new(
53
+ key: :config_path,
54
+ env_name: 'FL_CHARLES_CONFIG_PATH',
55
+ description: 'Path to a simplified Charles YAML config (see example/charles.yml), used to generate a Charles .config file at runtime',
56
+ default_value: 'charles.yml',
57
+ optional: true,
58
+ type: String
59
+ ),
60
+ FastlaneCore::ConfigItem.new(
61
+ key: :registered_name,
62
+ env_name: 'FL_CHARLES_REGISTERED_NAME',
63
+ description: 'Registered name for your Charles Proxy license',
64
+ optional: true,
65
+ type: String
66
+ ),
67
+ FastlaneCore::ConfigItem.new(
68
+ key: :registered_key,
69
+ env_name: 'FL_CHARLES_REGISTERED_KEY',
70
+ description: 'License key for your Charles Proxy registration',
71
+ optional: true,
72
+ sensitive: true,
73
+ type: String
74
+ ),
75
+ FastlaneCore::ConfigItem.new(
76
+ key: :ip_ranges,
77
+ env_name: 'FL_CHARLES_IP_RANGES',
78
+ description: 'IP ranges permitted to access the proxy, in CIDR notation or as bare IPs (treated as /32)',
79
+ optional: true,
80
+ type: Array,
81
+ default_value: []
82
+ )
83
+ ]
84
+ end
85
+
86
+ def self.is_supported?(platform)
87
+ true
88
+ end
89
+
90
+ def self.example_code
91
+ [
92
+ 'charles # Use default paths',
93
+ 'charles(app_path: "/path/to/Charles.app/Contents/MacOS/Charles")',
94
+ 'charles(config_path: "/path/to/charles.yml")',
95
+ 'charles(app_path: "/custom/path/to/Charles", config_path: "/custom/path/to/charles.yml")'
96
+ ]
97
+ end
98
+
99
+ def self.category
100
+ :misc
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,247 @@
1
+ require 'fastlane_core/ui/ui'
2
+ require 'yaml'
3
+ require 'rexml/document'
4
+
5
+ module Fastlane
6
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
7
+
8
+ module Helper
9
+ class CharlesHelper
10
+ SERIALISATION_VERSION = '2.0'.freeze
11
+ ACCEPTED_EULA_VERSION = '20240608'.freeze
12
+ DEFAULT_SSL_PORT = 443
13
+ DEFAULT_CIDR_PREFIX = 32
14
+ ALL_IP_RANGES_ALIAS = 'all'.freeze
15
+ ALL_IP_RANGES_CIDR = '0.0.0.0/0'.freeze
16
+ CIDR_PATTERN = %r{\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:/(\d{1,2}))?\z}
17
+
18
+ # Charles's standard location matcher (used by recording, and future
19
+ # tools like block/allow lists) supports protocol/host/port/path/query,
20
+ # each optional — Charles treats a blank field as "match all values".
21
+ # A YAML entry is either a bare hostname string, shorthand for
22
+ # `{host: <string>}`, or a Hash with any subset of those keys, e.g.:
23
+ # - protocol: https
24
+ # host: "api.example.com"
25
+ # path: "/debug"
26
+ LOCATION_FIELDS = %i[protocol host port path query].freeze
27
+
28
+ # Reads a simplified Charles YAML config (see example/charles.yml) and
29
+ # returns the equivalent Charles `.config` XML as a String.
30
+ #
31
+ # `registered_name`/`registered_key` (a Charles Proxy license) are
32
+ # per-developer secrets, not part of the committed YAML — they're
33
+ # passed in separately so they can be sourced from env vars instead.
34
+ #
35
+ # `ip_ranges` are runtime/per-developer CIDR ranges (e.g. a developer's
36
+ # own machine), merged with any `access_control.ip_ranges` committed in
37
+ # the YAML (e.g. a shared corporate subnet, or "all" for 0.0.0.0/0).
38
+ def self.generate_config_xml(yaml_path, registered_name: nil, registered_key: nil, ip_ranges: [])
39
+ config = load_config(yaml_path)
40
+ root = build_configuration_element(config)
41
+ combined_ip_ranges = merge_ip_ranges(config['access_control'] || {}, ip_ranges)
42
+ build_access_control_configuration(root, combined_ip_ranges)
43
+ build_startup_configuration(root)
44
+ build_registration_configuration(root, registered_name, registered_key)
45
+ xml_header + format_element(root)
46
+ end
47
+
48
+ # Loads and validates the YAML config, raising a clear UI.user_error!
49
+ # (instead of a raw Ruby exception) for the ways a hand-edited
50
+ # charles.yml commonly goes wrong: missing file, invalid YAML syntax,
51
+ # or valid YAML that isn't a Hash at the top level (e.g. a bare list).
52
+ def self.load_config(yaml_path)
53
+ UI.user_error!("Charles YAML config not found at #{yaml_path.inspect}") unless File.exist?(yaml_path)
54
+
55
+ config = begin
56
+ YAML.load_file(yaml_path)
57
+ rescue Psych::SyntaxError => e
58
+ UI.user_error!("Charles YAML config at #{yaml_path.inspect} is not valid YAML: #{e.message}")
59
+ end
60
+
61
+ config ||= {}
62
+ unless config.kind_of?(Hash)
63
+ UI.user_error!("Charles YAML config at #{yaml_path.inspect} must be a mapping (Hash) at the top level, got #{config.class}")
64
+ end
65
+
66
+ config
67
+ end
68
+
69
+ def self.xml_header
70
+ "<?xml version='1.0' encoding='UTF-8' ?>\n<?charles serialisation-version='#{SERIALISATION_VERSION}' ?>\n"
71
+ end
72
+
73
+ def self.build_configuration_element(config)
74
+ root = REXML::Element.new('configuration')
75
+ build_proxy_configuration(root, config['proxy'] || {})
76
+ build_recording_configuration(root, config['recording'] || {})
77
+ root
78
+ end
79
+
80
+ def self.format_element(element)
81
+ output = ''
82
+ formatter = REXML::Formatters::Pretty.new(2)
83
+ formatter.compact = true
84
+ formatter.write(element, output)
85
+ output
86
+ end
87
+
88
+ def self.build_proxy_configuration(root, proxy)
89
+ proxy_el = root.add_element('proxyConfiguration')
90
+ proxy_el.add_element('enableSOCKSProxy').text = proxy.fetch('enable_socks', true).to_s
91
+
92
+ ssl_includes, ssl_excludes = normalize_ssl_config(proxy['ssl'])
93
+ build_ssl_locations(proxy_el, 'sslLocations', ssl_includes)
94
+ build_ssl_locations(proxy_el, 'sslExcludeLocations', ssl_excludes)
95
+ end
96
+
97
+ def self.build_ssl_locations(proxy_el, element_name, entries)
98
+ return if entries.empty?
99
+
100
+ patterns_el = proxy_el.add_element(element_name).add_element('locationPatterns')
101
+ entries.each do |entry|
102
+ host, port = normalize_ssl_entry(entry)
103
+ location_el = patterns_el.add_element('locationMatch').add_element('location')
104
+ location_el.add_element('host').text = host
105
+ location_el.add_element('port').text = port.to_s
106
+ end
107
+ end
108
+
109
+ def self.build_recording_configuration(root, recording)
110
+ record_hosts = Array(recording['hosts'])
111
+ return if record_hosts.empty?
112
+
113
+ patterns_el = root.add_element('recordingConfiguration')
114
+ .add_element('recordHosts')
115
+ .add_element('locationPatterns')
116
+ record_hosts.each do |entry|
117
+ match_el = patterns_el.add_element('locationMatch')
118
+ build_location_element(match_el, normalize_location_entry(entry))
119
+ end
120
+ end
121
+
122
+ def self.build_access_control_configuration(root, ip_ranges)
123
+ ip_ranges = Array(ip_ranges)
124
+ return if ip_ranges.empty?
125
+
126
+ ip_ranges_el = root.add_element('accessControlConfiguration').add_element('ipRanges')
127
+ ip_ranges.each do |cidr|
128
+ ip_octets, mask_octets = parse_cidr(cidr)
129
+ range_el = ip_ranges_el.add_element('ipRange')
130
+ ip_el = range_el.add_element('ip')
131
+ ip_octets.each { |octet| ip_el.add_element('int').text = octet.to_s }
132
+ mask_el = range_el.add_element('mask')
133
+ mask_octets.each { |octet| mask_el.add_element('int').text = octet.to_s }
134
+ end
135
+ end
136
+
137
+ def self.build_startup_configuration(root)
138
+ root.add_element('startupConfiguration').add_element('acceptedEulaVersion').text = ACCEPTED_EULA_VERSION
139
+ end
140
+
141
+ def self.build_registration_configuration(root, registered_name, registered_key)
142
+ return if registered_name.nil? && registered_key.nil?
143
+
144
+ if registered_name.nil? || registered_key.nil?
145
+ UI.user_error!('Both registered_name and registered_key must be provided to register Charles')
146
+ end
147
+
148
+ registration_el = root.add_element('registrationConfiguration')
149
+ registration_el.add_element('name').text = registered_name
150
+ registration_el.add_element('key').text = registered_key
151
+ end
152
+
153
+ # `proxy.ssl` is either a plain list (shorthand for an include-only
154
+ # list, matching Charles's own default), or a Hash with `include`/
155
+ # `exclude` keys for the rarer case where some hosts need to be
156
+ # carved out of SSL decryption, e.g.:
157
+ # ssl:
158
+ # include:
159
+ # - "*.example.com"
160
+ # exclude:
161
+ # - "exclude-ssl-host.com"
162
+ def self.normalize_ssl_config(ssl)
163
+ if ssl.kind_of?(Hash)
164
+ [Array(ssl['include']), Array(ssl['exclude'])]
165
+ else
166
+ [Array(ssl), []]
167
+ end
168
+ end
169
+
170
+ # Each `proxy.ssl` (include/exclude) entry is either a bare hostname
171
+ # string (assumed to use the default SSL port), or a `{host:, port:}`
172
+ # Hash for the rare case where a host needs a non-standard port, e.g.:
173
+ # - host: "internal.example.com"
174
+ # port: 8443
175
+ def self.normalize_ssl_entry(entry)
176
+ if entry.kind_of?(Hash)
177
+ [entry['host'] || entry[:host], entry['port'] || entry[:port] || DEFAULT_SSL_PORT]
178
+ else
179
+ [entry, DEFAULT_SSL_PORT]
180
+ end
181
+ end
182
+
183
+ def self.normalize_location_entry(entry)
184
+ return { host: entry } unless entry.kind_of?(Hash)
185
+
186
+ LOCATION_FIELDS.each_with_object({}) do |field, attrs|
187
+ value = entry[field.to_s] || entry[field]
188
+ attrs[field] = value unless value.nil?
189
+ end
190
+ end
191
+
192
+ # Only emits the sub-elements a location entry actually specifies,
193
+ # matching how Charles itself omits blank ("match all") fields.
194
+ def self.build_location_element(parent, attrs)
195
+ location_el = parent.add_element('location')
196
+ LOCATION_FIELDS.each do |field|
197
+ value = attrs[field]
198
+ next if value.nil?
199
+
200
+ location_el.add_element(field.to_s).text = value.to_s
201
+ end
202
+ location_el
203
+ end
204
+
205
+ # Combines `access_control.ip_ranges` committed in the YAML with
206
+ # `ip_ranges` passed in at runtime, normalizing the "all" alias to
207
+ # 0.0.0.0/0 and removing duplicates.
208
+ def self.merge_ip_ranges(access_control, runtime_ip_ranges)
209
+ committed = Array(access_control['ip_ranges'])
210
+ (committed + Array(runtime_ip_ranges)).map { |entry| normalize_ip_range(entry) }.uniq
211
+ end
212
+
213
+ def self.normalize_ip_range(entry)
214
+ entry.to_s.strip.downcase == ALL_IP_RANGES_ALIAS ? ALL_IP_RANGES_CIDR : entry
215
+ end
216
+
217
+ # Converts a CIDR string (e.g. "10.0.1.20/32") into the ip/mask octet
218
+ # arrays Charles's accessControlConfiguration expects. A bare IP with
219
+ # no "/prefix" (e.g. "10.0.1.20") is treated as /32, i.e. that single
220
+ # host.
221
+ def self.parse_cidr(cidr)
222
+ ip_octets, prefix_length = validate_cidr!(cidr)
223
+
224
+ mask_int = prefix_length.zero? ? 0 : (0xFFFFFFFF << (32 - prefix_length)) & 0xFFFFFFFF
225
+ mask_octets = [24, 16, 8, 0].map { |shift| (mask_int >> shift) & 0xFF }
226
+
227
+ [ip_octets, mask_octets]
228
+ end
229
+
230
+ # Matches `cidr` against CIDR_PATTERN and returns the [ip_octets,
231
+ # prefix_length], raising a clear UI.user_error! for any of the ways
232
+ # a hand-typed ip_ranges entry commonly goes wrong.
233
+ def self.validate_cidr!(cidr)
234
+ match = CIDR_PATTERN.match(cidr.to_s)
235
+ UI.user_error!("Invalid ip_ranges entry #{cidr.inspect}: expected CIDR notation (e.g. \"10.0.1.20/32\") or a bare IP") unless match
236
+
237
+ ip_octets = match.captures[0, 4].map(&:to_i)
238
+ UI.user_error!("Invalid ip_ranges entry #{cidr.inspect}: each octet must be 0-255") if ip_octets.any? { |octet| octet > 255 }
239
+
240
+ prefix_length = (match[5] || DEFAULT_CIDR_PREFIX.to_s).to_i
241
+ UI.user_error!("Invalid ip_ranges entry #{cidr.inspect}: prefix must be 0-32") if prefix_length > 32
242
+
243
+ [ip_octets, prefix_length]
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,5 @@
1
+ module Fastlane
2
+ module Charles
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ require 'fastlane/plugin/charles/version'
2
+
3
+ module Fastlane
4
+ module Charles
5
+ # Return all .rb files inside the "actions" and "helper" directory
6
+ def self.all_classes
7
+ Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
8
+ end
9
+ end
10
+ end
11
+
12
+ # By default we want to import all available actions and helpers
13
+ # A plugin can contain any number of actions and plugins
14
+ Fastlane::Charles.all_classes.each do |current|
15
+ require current
16
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastlane-plugin-charles
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jake Krog
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Runs Charles Proxy from a fastlane lane, generating its .config file
13
+ at runtime from a simplified YAML config that a team can commit and share, instead
14
+ of hand-editing the XML Charles itself exports.
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - CHANGELOG.md
20
+ - LICENSE
21
+ - README.md
22
+ - lib/fastlane/plugin/charles.rb
23
+ - lib/fastlane/plugin/charles/actions/charles_action.rb
24
+ - lib/fastlane/plugin/charles/helper/charles_helper.rb
25
+ - lib/fastlane/plugin/charles/version.rb
26
+ homepage: https://github.com/jakekrog/fastlane-plugin-charles
27
+ licenses:
28
+ - MIT
29
+ metadata:
30
+ bug_tracker_uri: https://github.com/jakekrog/fastlane-plugin-charles/issues
31
+ changelog_uri: https://github.com/jakekrog/fastlane-plugin-charles/blob/main/CHANGELOG.md
32
+ rubygems_mfa_required: 'true'
33
+ source_code_uri: https://github.com/jakekrog/fastlane-plugin-charles
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '3.0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 3.6.9
49
+ specification_version: 4
50
+ summary: Run Charles HTTP proxy
51
+ test_files: []