Skip to content

Commit 24e22a6

Browse files
authored
feat(capacitor): enable NSAppTransportSecurity settings for live reload if needed (#4851)
1 parent 615d1f9 commit 24e22a6

File tree

2 files changed

+166
-0
lines changed
  • packages/@ionic/cli/src

2 files changed

+166
-0
lines changed

packages/@ionic/cli/src/commands/capacitor/base.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { runCommand } from '../../lib/executor';
1313
import type { CapacitorCLIConfig, Integration as CapacitorIntegration } from '../../lib/integrations/capacitor'
1414
import { ANDROID_MANIFEST_FILE, CapacitorAndroidManifest } from '../../lib/integrations/capacitor/android';
1515
import { CAPACITOR_CONFIG_JSON_FILE, CapacitorJSONConfig, CapacitorConfig } from '../../lib/integrations/capacitor/config';
16+
import { CapacitorIosInfo, IOS_INFO_FILE } from '../../lib/integrations/capacitor/ios';
1617
import { generateOptionsForCapacitorBuild } from '../../lib/integrations/capacitor/utils';
1718
import { createPrefixedWriteStream } from '../../lib/utils/logger';
1819
import { pkgManagerArgs } from '../../lib/utils/npm';
@@ -65,6 +66,18 @@ export abstract class CapacitorCommand extends Command {
6566
return path.resolve(this.integration.root, srcDir, ANDROID_MANIFEST_FILE);
6667
}
6768

69+
async getiOSAppInfo(): Promise<CapacitorIosInfo> {
70+
const p = await this.getiOSAppInfoPath();
71+
return CapacitorIosInfo.load(p);
72+
}
73+
74+
async getiOSAppInfoPath(): Promise<string> {
75+
const cli = await this.getCapacitorCLIConfig();
76+
const srcDir = cli?.ios.nativeTargetDirAbs ?? 'ios/App/App';
77+
78+
return path.resolve(this.integration.root, srcDir, IOS_INFO_FILE);
79+
}
80+
6881
async getGeneratedConfigDir(platform: string): Promise<string> {
6982
const cli = await this.getCapacitorCLIConfig();
7083

@@ -236,6 +249,17 @@ export abstract class CapacitorCommand extends Command {
236249

237250
manifest.enableCleartextTraffic();
238251
await manifest.save();
252+
}
253+
254+
if (platform === 'ios' && !options['external']) {
255+
const appInfo = await this.getiOSAppInfo();
256+
257+
onBeforeExit(async () => {
258+
await appInfo.reset();
259+
})
260+
261+
appInfo.disableAppTransportSecurity();
262+
await appInfo.save();
239263
}
240264
} catch (e) {
241265
if (e instanceof RunnerException) {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { readFile, writeFile, unlink } from "@ionic/utils-fs";
2+
import * as et from "elementtree";
3+
4+
export const IOS_INFO_FILE = "Info.plist";
5+
6+
export class CapacitorIosInfo {
7+
protected _doc?: et.ElementTree;
8+
protected origInfoPlistContent?: string;
9+
protected saving = false;
10+
11+
constructor(readonly plistPath: string) {}
12+
13+
get origPlistPath(): string {
14+
return `${this.plistPath}.orig`;
15+
}
16+
17+
get doc(): et.ElementTree {
18+
if (!this._doc) {
19+
throw new Error("No doc loaded.");
20+
}
21+
22+
return this._doc;
23+
}
24+
25+
static async load(plistPath: string): Promise<CapacitorIosInfo> {
26+
if (!plistPath) {
27+
throw new Error(`Must supply file path for ${IOS_INFO_FILE}.`);
28+
}
29+
30+
const conf = new CapacitorIosInfo(plistPath);
31+
await conf.reload();
32+
33+
return conf;
34+
}
35+
36+
disableAppTransportSecurity() {
37+
const rootDict = this.getDictRoot();
38+
39+
let valueDict = this.getValueForKey(rootDict,"NSAppTransportSecurity");
40+
if (valueDict) {
41+
const value = this.getValueForKey(valueDict, "NSAllowsArbitraryLoads")
42+
if (value) {
43+
value.tag = "true"
44+
} else {
45+
et.SubElement(valueDict, "true")
46+
}
47+
} else {
48+
const newKey = et.SubElement(rootDict, "key");
49+
newKey.text = "NSAppTransportSecurity";
50+
51+
const newDict = et.SubElement(rootDict, "dict");
52+
const newDictKey = et.SubElement(newDict, "key")
53+
newDictKey.text = "NSAllowsArbitraryLoads";
54+
et.SubElement(newDict, "true")
55+
}
56+
}
57+
58+
59+
private getValueForKey(root: et.Element, key: string): et.Element | null {
60+
const children = root.getchildren();
61+
let keyFound = false;
62+
63+
for (const element of children) {
64+
if (keyFound) {
65+
keyFound = false;
66+
67+
return element;
68+
}
69+
70+
if ((element.tag === 'key') && element.text === key) {
71+
keyFound = true;
72+
}
73+
}
74+
75+
return null;
76+
}
77+
78+
private getDictRoot(): et.Element {
79+
const root = this.doc.getroot();
80+
81+
if (root.tag !== "plist") {
82+
throw new Error(`Info.plist is not a valid plist file because the root is not a <plist> tag`);
83+
}
84+
85+
const rootDict = root.find('./dict');
86+
if (!rootDict) {
87+
throw new Error(`Info.plist is not a valid plist file because the first child is not a <dict> tag`);
88+
}
89+
90+
return rootDict;
91+
}
92+
93+
async reset(): Promise<void> {
94+
const origInfoPlistContent = await readFile(this.origPlistPath, {
95+
encoding: "utf8",
96+
});
97+
98+
if (!this.saving) {
99+
this.saving = true;
100+
await writeFile(this.plistPath, origInfoPlistContent, {
101+
encoding: "utf8",
102+
});
103+
await unlink(this.origPlistPath);
104+
this.saving = false;
105+
}
106+
}
107+
108+
async save(): Promise<void> {
109+
if (!this.saving) {
110+
this.saving = true;
111+
112+
if (this.origInfoPlistContent) {
113+
await writeFile(this.origPlistPath, this.origInfoPlistContent, {
114+
encoding: "utf8",
115+
});
116+
this.origInfoPlistContent = undefined;
117+
}
118+
119+
await writeFile(this.plistPath, this.write(), { encoding: "utf8" });
120+
121+
this.saving = false;
122+
}
123+
}
124+
125+
protected async reload(): Promise<void> {
126+
this.origInfoPlistContent = await readFile(this.plistPath, {
127+
encoding: "utf8",
128+
});
129+
130+
try {
131+
this._doc = et.parse(this.origInfoPlistContent);
132+
} catch (e: any) {
133+
throw new Error(`Cannot parse ${IOS_INFO_FILE} file: ${e.stack ?? e}`);
134+
}
135+
}
136+
137+
protected write(): string {
138+
const contents = this.doc.write({ indent: 4 });
139+
140+
return contents;
141+
}
142+
}

0 commit comments

Comments
 (0)