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 +7 -0
- data/CHANGELOG.md +42 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/lib/fastlane/plugin/charles/actions/charles_action.rb +104 -0
- data/lib/fastlane/plugin/charles/helper/charles_helper.rb +247 -0
- data/lib/fastlane/plugin/charles/version.rb +5 -0
- data/lib/fastlane/plugin/charles.rb +16 -0
- metadata +51 -0
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
|
+
[](https://rubygems.org/gems/fastlane-plugin-charles)
|
|
4
|
+
[](https://rubygems.org/gems/fastlane-plugin-charles)
|
|
5
|
+
[](https://github.com/jakekrog/fastlane-plugin-charles/actions/workflows/test.yml)
|
|
6
|
+
[](https://github.com/jakekrog/fastlane-plugin-charles/actions/workflows/pre-commit.yml)
|
|
7
|
+
[](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,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: []
|