├── .github
└── ISSUE_TEMPLATE.md
├── .gitignore
├── .gitmodules
├── .travis.yml
├── LICENSE
├── README.md
├── V2RayX.xcodeproj
└── project.pbxproj
├── V2RayX
├── AdvancedWindow.xib
├── AdvancedWindowController.h
├── AdvancedWindowController.m
├── AppDelegate.h
├── AppDelegate.m
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── vx 512.png
│ │ ├── vx128.png
│ │ ├── vx16.png
│ │ ├── vx256.png
│ │ ├── vx32.png
│ │ └── vx64.png
│ ├── Contents.json
│ ├── config_duplicate.imageset
│ │ ├── Contents.json
│ │ ├── duplicate-document.png
│ │ └── duplicate-document16.png
│ ├── statusBarIcon.imageset
│ │ ├── Contents.json
│ │ ├── b16.png
│ │ └── b32.png
│ └── statusBarIcon_disabled.imageset
│ │ ├── Contents.json
│ │ ├── g16.png
│ │ └── g32.png
├── Base.lproj
│ └── MainMenu.xib
├── ConfigImporter.h
├── ConfigImporter.m
├── ConfigWindow.xib
├── ConfigWindowController.h
├── ConfigWindowController.m
├── Credits.rtf
├── Info.plist
├── MutableDeepCopying.h
├── NSData+AES256Encryption.h
├── NSData+AES256Encryption.m
├── ServerProfile.h
├── ServerProfile.m
├── TransportWindow.xib
├── TransportWindowController.h
├── TransportWindowController.m
├── config-sample.plist
├── config-sample_new.plist
├── dlcore.sh
├── install_helper.sh
├── main.m
├── simple.pac
├── tcp_http_header_example.txt
├── utilities.h
└── utilities.m
├── compilefromsource.sh
├── jsonplist
└── main.m
├── logic.png
├── prepare_zip.sh
├── v2rayx_sysconf
├── main.m
└── sysconf_version.h
└── vx.ggb
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: V2RayX issue 模版
3 | ---
4 |
5 | 请回答下列问题。不按模板发的 issue 将直接被关闭。
6 |
7 | 0) 如果你没有遇到任何错误和不正常,请在此处回答你想发表的内容:
8 |
9 | 1) 你正在使用哪个版本的 V2RayX,你的 macOS 系统版本?
10 |
11 | 2) 如果你在操作 V2RayX 的时候遇到程序崩溃/无反应,请描述你的操作,之后在命令行(终端.app)里运行`/Applications/V2RayX.app/Contents/MacOS/V2RayX`,重复上述操作直至软件崩溃,把终端里的输出贴到下面指定的地方。
12 |
13 | ```
14 | 在这里贴上终端的输出
15 | ```
16 |
17 | 3) 如果 V2RayX 没有发生崩溃,但是有一些不正常行为,比如上次勾选了 mux,再次打开,mux 的钩没有了。请描述你遇到的不正常行为。
18 |
19 | 4) 如果 V2RayX 一切正常,但网络依然不如你所预料,请首先把 log level 切换到 debug,再次访问你想访问的网站。然后点击 V2RayX 的 `view current config.json`,将弹出的浏览器内的配置文件粘贴到下面指定的地方,但是隐藏掉ip/端口/id等信息。
20 |
21 | ```javascript
22 | 在这里粘贴配置文件内容
23 | ```
24 |
25 | 然后点击 V2RayX 的`view log`,把 error.log 中的输出贴到下方:
26 |
27 | ```
28 | 在这里贴上 error.log
29 | ```
30 |
31 | 5) 其他你认为可以帮助开发者和你一起解决问题的信息:
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | DerivedData/
2 | .DS_Store
3 | V2RayX.xcodeproj/project.xcworkspace
4 | V2RayX.xcodeproj/xcuserdata/
5 | build/
6 | v2ray-core-bin/
7 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "GCDWebServer"]
2 | path = GCDWebServer
3 | url = https://github.com/swisspol/GCDWebServer.git
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | os: osx
2 | language: objective-c
3 | sudo: false
4 | install: true
5 | script: xcodebuild -project V2RayX.xcodeproj -target V2RayX -configuration Release
6 |
7 | after_success:
8 | - isbeta=$(git describe --abbrev=0 --tags | grep beta)
9 |
10 | before_deploy:
11 | - sh ./prepare_zip.sh
12 |
13 | deploy:
14 | - provider: releases
15 | prerelease: true
16 | api_key:
17 | secure: HjwZ8C70qYeozvaEUvKqDX42T26pRYiQBrTvzbqy8Q+BDWJZ7vNZhSwgt+oYGBywgpk0bM/Ao3al6YDlTE86bMiRUEGSFjRWo2Oquo4N5ydp5cmzT6sVigrLiviufAMrCrCljh0OEt/hEN7wf7zJ8SZKFCLOi8AhTkJu8yEuFdx/2Zd/VSOs6AFp0s1uGrfd3UmwxWfGcKatpYb4GcCwZk2PrECc0qkAP9UzGjemU1cYzcICi+cXl/NoMeLRftHSZaGl+zg95bvddSlNvyqvPwH/00hnh6kpjdyKMDQ071eFE+9NIShglm5yS9TcR3+fE6WfWU9SjTOnT2IfggvXfogcJWd7k4/AwFoAkezNiV0IMKXl2163T17pfFxVQo56oy9HtMts3bw5TapL2YiJa1PKzluSc5ijNcGdDh/7GIejne9lyTeHNhnE/Acmlt8Mu9N+gSj04R4sqN/GfTGzmlPC4kXB08RLg5ycHksGyf4YOLYe9wOEBN4UaSqepFsWqrrufj16l4dqTdacLdt2dJtkPsqCCiUu+nfVVZP9uibIMqWjBi+Fp2Rz+RIvaqtoQrMy+b1y/YwcVz9cJ29OMwU5hY6MehN5pqknwMhsDk1vH5m93swpL6wfM6CT7NjambVfPGffX8zYp/1l/CbBelzjaxZ/CbV4GHXkYHJ923s=
18 | file: build/Debug/V2RayX.app.zip
19 | skip_cleanup: true
20 | on:
21 | repo: Cenmrev/V2RayX
22 | tags: true
23 | condition: "\"$isbeta\" != \"\""
24 |
25 | - provider: releases
26 | api_key:
27 | secure: HjwZ8C70qYeozvaEUvKqDX42T26pRYiQBrTvzbqy8Q+BDWJZ7vNZhSwgt+oYGBywgpk0bM/Ao3al6YDlTE86bMiRUEGSFjRWo2Oquo4N5ydp5cmzT6sVigrLiviufAMrCrCljh0OEt/hEN7wf7zJ8SZKFCLOi8AhTkJu8yEuFdx/2Zd/VSOs6AFp0s1uGrfd3UmwxWfGcKatpYb4GcCwZk2PrECc0qkAP9UzGjemU1cYzcICi+cXl/NoMeLRftHSZaGl+zg95bvddSlNvyqvPwH/00hnh6kpjdyKMDQ071eFE+9NIShglm5yS9TcR3+fE6WfWU9SjTOnT2IfggvXfogcJWd7k4/AwFoAkezNiV0IMKXl2163T17pfFxVQo56oy9HtMts3bw5TapL2YiJa1PKzluSc5ijNcGdDh/7GIejne9lyTeHNhnE/Acmlt8Mu9N+gSj04R4sqN/GfTGzmlPC4kXB08RLg5ycHksGyf4YOLYe9wOEBN4UaSqepFsWqrrufj16l4dqTdacLdt2dJtkPsqCCiUu+nfVVZP9uibIMqWjBi+Fp2Rz+RIvaqtoQrMy+b1y/YwcVz9cJ29OMwU5hY6MehN5pqknwMhsDk1vH5m93swpL6wfM6CT7NjambVfPGffX8zYp/1l/CbBelzjaxZ/CbV4GHXkYHJ923s=
28 | file: build/Release/V2RayX.app.zip
29 | skip_cleanup: true
30 | on:
31 | repo: Cenmrev/V2RayX
32 | tags: true
33 | condition: "\"$isbeta\" = \"\""
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # V2RayX: A simple GUI for V2Ray on macOS
2 |
3 | [](https://travis-ci.org/Cenmrev/V2RayX)
4 |
5 | ## What is V2Ray?
6 |
7 | __READ THIS__: [Project V2Ray](http://www.v2ray.com).
8 |
9 | __YOU SHOULD READ V2RAY'S OFFICIAL INSTRUCTION BEFORE USING V2RAYX!__
10 |
11 | Other V2Ray clients on macOS: [V2RayU](https://github.com/yanue/v2rayu).
12 | (Not related to or endorsed by authors of this repo. USE AT YOUR OWN RISK.)
13 |
14 | ## Download V2RayX
15 |
16 | Download from [Releases](https://github.com/Cenmrev/V2RayX/releases). (compiled by [travis-ci.org](https://travis-ci.org/Cenmrev/V2RayX)).
17 |
18 | By [Homebrew-Cask](https://caskroom.github.io/).
19 |
20 | ```sh
21 | brew cask install v2rayx
22 | ```
23 |
24 | ## How to build
25 |
26 | V2RayX.app is built by running one of the following commands in your terminal. You can install this via the command-line with curl.
27 |
28 | `sh -c "$(curl -fsSL https://raw.githubusercontent.com/Cenmrev/V2RayX/master/compilefromsource.sh)"`
29 |
30 | or step by step:
31 |
32 | `git clone --recursive https://github.com/Cenmrev/V2RayX.git`
33 |
34 | open V2RayX.xcodeproj and use Xcode to build V2RayX.
35 |
36 | ## How does V2RayX work
37 |
38 | V2RayX provides a GUI to generate the config file for V2Ray. It includes V2Ray's binary executable in the app bundle. V2RayX starts and stops V2Ray with `launchd` of macOS.
39 |
40 | V2RayX also allows users to change system proxy settings and switch proxy servers on the macOS menu bar.
41 |
42 | As default, V2RayX will open a socks5 proxy at port `1081` as the main inbound, as well as a http proxy at port `8001` as an inboundDetour.
43 |
44 | V2RayX provide three modes:
45 | * Global Mode: V2RayX asks macOS to route all internet traffic to v2ray core if the network traffic obeys operating system's network rules.
46 | * PAC Mode: macOS will determine the routing based on a pac file and some traffic may be routed to v2ray core.
47 | * Manual Mode: V2RayX will not modify any macOS network settings, but only start or stop v2ray core.
48 |
49 | Options in menu list `Routing Rule` determine how v2ray core deals with incoming traffic. Core routing rules apply to all three modes above.
50 |
51 | ### auto-run on login
52 |
53 | Open macOS System Preferences -> Users & Group -> Login Items, add V2RayX.app to
54 | the list.
55 |
56 | ### manually update v2ray-core
57 | replace `V2RayX.app/Contents/Resources/v2ray` with the newest v2ray
58 | version from [v2ray-core
59 | repo](https://github.com/v2ray/v2ray-core/releases). However, compatibility is not guaranteed.
60 |
61 | ### Uninstall
62 |
63 | V2RayX will create the following files and folders:
64 |
65 | * `/Library/Application Support/V2RayX`
66 | * `~/Library/Application Support/V2RayX`
67 | * `~/Library/Preferences/cenmrev.V2RayX.plist`
68 |
69 | So, to totally uninstall V2RayX, just delete V2RayX.app and the files above. :)
70 |
71 | ## Acknowledge
72 |
73 | V2RayX uses [GCDWebServer](https://github.com/swisspol/GCDWebServer) to provide a local pac server. V2RayX also uses many ideas and codes from [ShadowsocksX](https://github.com/shadowsocks/shadowsocks-iOS/tree/master), especially, the codes of [v2rays_sysconfig](https://github.com/Cenmrev/V2RayX/blob/master/v2rayx_sysconf/main.m) are simply copied from [shadowsocks_sysconf](https://github.com/shadowsocks/shadowsocks-iOS/blob/master/shadowsocks_sysconf/main.m) with some modifications.
74 |
75 | ## Donation
76 |
77 | If Project V2Ray or V2RayX helped you, you can also help us by donation __in your will__. To donate to Project V2Ray, you may refer to [this page](https://www.v2ray.com/chapter_00/02_donate.html).
78 |
79 | ## Disclaimer
80 |
81 | This tool is mainly for personal usage. For professional users and technique
82 | support, commercial software like proxifier is recommended. Please refer to [#60](https://github.com/Cenmrev/V2RayX/issues/60#issuecomment-369531443).
83 |
84 | The developer does not major in CS nor Software Engineer and currently is busy with grad school courses. So V2rayX will not be updated frequently. Users can replace V2RayX.app/Contents/Resources/v2ray with the newest v2ray-core downloaded from [https://github.com/v2ray/v2ray-core/releases](https://github.com/v2ray/v2ray-core/releases).
85 |
86 | The developer currently does not have enough time to add more features to V2RayX, nor to merge PRs. However, forking and releasing your own version are always welcome.
87 |
--------------------------------------------------------------------------------
/V2RayX/AdvancedWindowController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AdvancedWindowController.h
3 | // V2RayX
4 | //
5 | //
6 |
7 | #import
8 | #import "ConfigWindowController.h"
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AdvancedWindowController : NSWindowController
13 |
14 | - (instancetype)initWithWindowNibName:(NSNibName)windowNibName parentController:(ConfigWindowController*)parent;
15 | @property (weak) IBOutlet NSTextField *checkLabel;
16 |
17 | @property (weak) IBOutlet NSTabView *mainTabView;
18 |
19 | //outbounds
20 | @property (weak) IBOutlet NSTableView *outboundTable;
21 | @property (unsafe_unretained) IBOutlet NSTextView *outboundJsonView;
22 | @property (weak) IBOutlet NSSegmentedControl *outboundAddControl;
23 | @property NSMutableArray* outbounds;
24 |
25 | // subscription
26 | @property (weak) IBOutlet NSTableView *subscriptionTable;
27 | @property (weak) IBOutlet NSSegmentedControl *subscriptionAddControl;
28 | @property NSMutableArray* subscriptions;
29 |
30 | //rules
31 | @property (strong) IBOutlet NSView *domainListEditView;
32 | @property (strong) IBOutlet NSView *ipListEditView;
33 |
34 | @property (strong) IBOutlet NSView *routingTagHelpView;
35 | @property (strong) IBOutlet NSView *domainIpHelpView;
36 |
37 | @property (weak) IBOutlet NSTableView *ruleSetTable;
38 | @property (weak) IBOutlet NSSegmentedControl *ruleSetAddControl;
39 | @property (weak) IBOutlet NSTextField *ruleSetNameField;
40 | @property (weak) IBOutlet NSPopUpButton *domainStrategyButton;
41 | @property (weak) IBOutlet NSTableView *ruleTable;
42 | @property (weak) IBOutlet NSSegmentedControl *ruleAddControl;
43 | @property (weak) IBOutlet NSButton *domainIpHelpButton;
44 |
45 | // enbale buttons
46 | @property (weak) IBOutlet NSButton *domainEnableButton;
47 | @property (weak) IBOutlet NSButton *ipEnableButton;
48 | @property (weak) IBOutlet NSButton *inboundEnableButton;
49 | @property (weak) IBOutlet NSButton *protocolEnableButton;
50 | @property (weak) IBOutlet NSButton *portEnableButton;
51 | @property (weak) IBOutlet NSButton *networkEnableButton;
52 |
53 | // fields
54 | @property (weak) IBOutlet NSButton *editDomainButton;
55 | @property (unsafe_unretained) IBOutlet NSTextView *domainTextView;
56 | @property (weak) IBOutlet NSButton *editIpButton;
57 | @property (unsafe_unretained) IBOutlet NSTextView *ipTextView;
58 | @property (weak) IBOutlet NSComboBox *inboundTagBox;
59 | @property (weak) IBOutlet NSPopUpButton *protocolButton;
60 | @property (weak) IBOutlet NSTextField *portField;
61 | @property (weak) IBOutlet NSPopUpButton *networkListButton;
62 | @property (weak) IBOutlet NSButton *saveIPListButton;
63 | @property (weak) IBOutlet NSButton *saveDomainListButton;
64 |
65 | @property (weak) IBOutlet NSButton *routeToHelpButton;
66 | @property (weak) IBOutlet NSComboBox *routeToBox;
67 | @property NSMutableArray* routingRuleSets;
68 |
69 | //config
70 | @property (weak) IBOutlet NSTableView *configTable;
71 | @property (weak) IBOutlet NSSegmentedControl *configAddControl;
72 | @property NSMutableArray* configs;
73 |
74 | // v2ray core
75 | @property (weak) IBOutlet NSTextField *corePathField;
76 | @property (weak) IBOutlet NSTextField *coreFileListField;
77 | @property (weak) IBOutlet NSPopUpButton *enableRestoreButton;
78 | @property BOOL enableRestore;
79 |
80 | // encryption
81 | @property (weak) IBOutlet NSButton *enableEncryptionButton;
82 | @property (weak) IBOutlet NSSecureTextField *encryptionKeyField;
83 | @property (weak) IBOutlet NSSecureTextField *encryptionKeyConfirmField;
84 | @property BOOL enableEncryption;
85 | @property NSString* encryptionKey;
86 | @property (weak) IBOutlet NSTextField *changeIndicatorField;
87 |
88 | @end
89 |
90 | NS_ASSUME_NONNULL_END
91 |
--------------------------------------------------------------------------------
/V2RayX/AdvancedWindowController.m:
--------------------------------------------------------------------------------
1 | //
2 | // AdvancedWindowController.m
3 | // V2RayX
4 | //
5 | //
6 |
7 | #import "AdvancedWindowController.h"
8 | #import "MutableDeepCopying.h"
9 | #include
10 |
11 | @interface AdvancedWindowController () {
12 | ConfigWindowController* configWindowController;
13 | //outbound
14 |
15 | }
16 |
17 | @property (strong) NSPopover* popover;
18 | @property NSInteger selectedOutbound;
19 | @property (atomic) NSInteger selectedRuleSet;
20 | @property (atomic) NSInteger selectedRule;
21 |
22 | @end
23 |
24 | @implementation AdvancedWindowController
25 |
26 | - (instancetype)initWithWindowNibName:(NSNibName)windowNibName parentController:(ConfigWindowController*)parent {
27 | self = [super initWithWindowNibName:windowNibName];
28 | if (self) {
29 | configWindowController = parent;
30 | }
31 | return self;
32 | }
33 |
34 | - (void)removeObservers {
35 | [self removeObserver:self forKeyPath:@"selectedOutbound"];
36 | [self removeObserver:self forKeyPath:@"selectedRuleSet"];
37 | [self removeObserver:self forKeyPath:@"selectedRule"];
38 | }
39 |
40 | - (void)windowDidLoad {
41 | [super windowDidLoad];
42 |
43 | // initialize UI
44 | [_outboundTable setFocusRingType:NSFocusRingTypeNone];
45 | [_ruleTable setFocusRingType:NSFocusRingTypeNone];
46 | [_ruleSetTable setFocusRingType:NSFocusRingTypeNone];
47 | [_configTable setFocusRingType:NSFocusRingTypeNone];
48 | [_subscriptionTable setFocusRingType:NSFocusRingTypeNone];
49 | [[self domainStrategyButton] removeAllItems];
50 | for(NSString* strategy in DOMAIN_STRATEGY_LIST) {
51 | [[self domainStrategyButton] addItemWithTitle:strategy];
52 | }
53 | [[self networkListButton] removeAllItems];
54 | for(NSString* network in ROUTING_NETWORK_LIST) {
55 | [[self networkListButton] addItemWithTitle:network];
56 | }
57 | [_protocolButton removeAllItems];
58 | for (NSString* p in SNIFFING_PROTOCOL) {
59 | [_protocolButton addItemWithTitle:p];
60 | }
61 | _outboundJsonView.automaticQuoteSubstitutionEnabled = false;
62 | _domainTextView.automaticQuoteSubstitutionEnabled = false;
63 | [_routeToBox removeAllItems];
64 | [_routeToBox addItemsWithObjectValues:RESERVED_TAGS];
65 | [_inboundTagBox removeAllItems];
66 | [_inboundTagBox addItemsWithObjectValues:@[@"socksinbound", @"httpinbound"]];
67 |
68 | // outbound
69 | [_outboundJsonView setFont:[NSFont fontWithName:@"Menlo" size:13]];
70 | [self addObserver:self
71 | forKeyPath:@"selectedOutbound"
72 | options:NSKeyValueObservingOptionNew
73 | context:nil];
74 |
75 | // rule
76 | [self addObserver:self
77 | forKeyPath:@"selectedRuleSet"
78 | options:NSKeyValueObservingOptionNew
79 | context:nil];
80 | [self addObserver:self
81 | forKeyPath:@"selectedRule"
82 | options:NSKeyValueObservingOptionNew
83 | context:nil];
84 |
85 | [[NSNotificationCenter defaultCenter] addObserver:self
86 | selector:@selector(textFieldDidChange:)
87 | name:NSControlTextDidChangeNotification
88 | object:_ruleSetNameField];
89 | [[NSNotificationCenter defaultCenter] addObserver:self
90 | selector:@selector(textFieldDidChange:)
91 | name:NSControlTextDidChangeNotification
92 | object:_routeToBox];
93 | [[NSNotificationCenter defaultCenter] addObserver:self
94 | selector:@selector(textFieldDidChange:)
95 | name:NSControlTextDidChangeNotification
96 | object:_portField];
97 |
98 |
99 | // core path
100 | self.corePathField.stringValue = [NSString stringWithFormat:@"%@/Library/Application Support/V2RayX/v2ray-core/",NSHomeDirectory()];
101 | self.enableRestore = configWindowController.enableRestore;
102 |
103 | self.enableEncryption = configWindowController.enableEncryption;
104 | self.encryptionKey = [[NSString alloc] initWithString:configWindowController.encryptionKey];
105 | // encryption
106 | _changeIndicatorField.stringValue = @"";
107 |
108 | [self fillData];
109 | }
110 |
111 | - (void)fillData {
112 | // outbound
113 | self.outbounds = [configWindowController.outbounds mutableCopy];
114 | _outboundJsonView.editable = self.outbounds.count > 0;
115 | if (self.outbounds.count > 0) {
116 | self.selectedOutbound = 0;
117 | } else {
118 | self.selectedOutbound = -1;
119 | }
120 | [_outboundTable reloadData];
121 | // subscriptions
122 | self.subscriptions = [configWindowController.subscriptions mutableCopy];
123 | [_subscriptionTable reloadData];
124 | // rules
125 | self.selectedRuleSet = 0;
126 | self.selectedRule = 0;
127 | self.routingRuleSets = [configWindowController.routingRuleSets mutableDeepCopy];
128 | // configs
129 | self.configs = [configWindowController.cusProfiles mutableDeepCopy];
130 | [_configTable reloadData];
131 | // core
132 | [_enableRestoreButton selectItemAtIndex:_enableRestore?1:0];
133 | }
134 |
135 | - (IBAction)ok:(id)sender {
136 |
137 | if (![self checkOutbound]) {
138 | return;
139 | }
140 | if (![self checkConfig]) {
141 | return;
142 | }
143 | [self removeObservers];
144 | [self.window.sheetParent endSheet:self.window returnCode:NSModalResponseOK];
145 | }
146 |
147 | - (IBAction)cancel:(id)sender {
148 | [self removeObservers];
149 | [self.window.sheetParent endSheet:self.window returnCode:NSModalResponseCancel];
150 | }
151 |
152 | // table data
153 | - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
154 | if (tableView == _outboundTable) {
155 | return [self.outbounds count];
156 | }
157 | if (tableView == _configTable) {
158 | return [self.configs count];
159 | }
160 | if (tableView == _ruleSetTable) {
161 | return self.routingRuleSets.count;
162 | }
163 | if (tableView == _ruleTable) {
164 | return [self.routingRuleSets[_selectedRuleSet][@"rules"] count];
165 | }
166 | if (tableView == _subscriptionTable) {
167 | return [self.subscriptions count];
168 | }
169 | return 0;
170 | }
171 |
172 | - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
173 | if (tableView == _outboundTable) {
174 | return self.outbounds[row][@"tag"];
175 | }
176 | if (tableView == _configTable) {
177 | return self.configs[row];
178 | }
179 | if (tableView == _ruleSetTable) {
180 | return self.routingRuleSets[row][@"name"];
181 | }
182 | if(tableView == _ruleTable) {
183 | NSUInteger ruleCount = [self.routingRuleSets[_selectedRuleSet][@"rules"] count];
184 | NSDictionary* rule = self.routingRuleSets[_selectedRuleSet][@"rules"][row];
185 | NSString* routeTo = rule[@"outboundTag"] ? rule[@"outboundTag"] : rule[@"balancerTag"];
186 | return row + 1 == ruleCount ? [NSString stringWithFormat:@"final:%@", routeTo] : [NSString stringWithFormat:@"%lu:%@", row, routeTo] ;
187 | }
188 | if (tableView == _subscriptionTable) {
189 | return self.subscriptions[row];
190 | }
191 | return @"";
192 | }
193 |
194 | // table delegate
195 | - (void)tableViewSelectionDidChange:(NSNotification *)notification {
196 | if (notification.object == _outboundTable) {
197 | if (_outboundTable.selectedRow != _selectedOutbound) {
198 | [self checkOutbound];
199 | } else {
200 | NSLog(@"do nothing");
201 | }
202 | }
203 | if (notification.object == _ruleSetTable) {
204 | self.selectedRuleSet = [_ruleSetTable selectedRow];
205 | [_ruleTable reloadData];
206 | }
207 | if (notification.object == _ruleTable) {
208 | self.selectedRule = [_ruleTable selectedRow];
209 | }
210 | }
211 |
212 | - (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
213 | if (tableView == _configTable) {
214 | self.configs[row] = object;
215 | } else if (tableView == _subscriptionTable) {
216 | self.subscriptions[row] = object;
217 | }
218 | }
219 |
220 | // bound
221 |
222 | - (BOOL)checkOutbound {
223 | if (_outbounds.count == 0) {
224 | return YES;
225 | }
226 | NSError *e;
227 | NSDictionary* newOutboud = [NSJSONSerialization JSONObjectWithData:[_outboundJsonView.string dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&e];
228 | if (e) {
229 | [self showAlert:@"NOT a valid json"];
230 | [_outboundTable selectRowIndexes:[NSIndexSet indexSetWithIndex:_selectedOutbound] byExtendingSelection:NO];
231 | return NO;
232 | } else {
233 | self.outbounds[_selectedOutbound] = newOutboud;
234 | self.selectedOutbound = _outboundTable.selectedRow;
235 | [_outboundTable reloadData];
236 | return YES;
237 | }
238 | }
239 |
240 | - (IBAction)addRemoveOutbound:(id)sender {
241 | if ([sender selectedSegment] == 0) {
242 | NSString* tagName = [NSString stringWithFormat:@"tag%lu", self.outbounds.count];
243 | [self.outbounds addObject:@{
244 | @"sendThrough": @"0.0.0.0",
245 | @"protocol": @"protocol name",
246 | @"settings": @{},
247 | @"tag": tagName,
248 | @"streamSettings": @{},
249 | @"mux": @{}
250 | }];
251 | if (_selectedOutbound == -1) {
252 | _selectedOutbound = 0;
253 | self.selectedOutbound = 0;
254 | }
255 | } else {
256 | if (_selectedOutbound >= 0 && _selectedOutbound < _outbounds.count) {
257 | [_outbounds removeObjectAtIndex:_selectedOutbound];
258 | self.selectedOutbound = MIN((NSInteger)_outbounds.count - 1, _selectedOutbound);
259 | }
260 | }
261 | [_outboundTable reloadData];
262 | _outboundJsonView.editable = _outbounds.count > 0;
263 | }
264 |
265 | // rules
266 |
267 | -(void)textFieldDidChange:(NSNotification *)notification {
268 | if (notification.object == _ruleSetNameField) {
269 | self.routingRuleSets[_selectedRuleSet][@"name"] = _ruleSetNameField.stringValue;
270 | }
271 | if (notification.object == _routeToBox) {
272 | NSMutableDictionary* rule =
273 | self.routingRuleSets[_selectedRuleSet][@"rules"][_selectedRule];
274 | if ([@"balance" isEqualToString:_routeToBox.stringValue]) {
275 | rule[@"balancerTag"] = @"balance";
276 | [rule removeObjectForKey:@"outboundTag"];
277 | } else {
278 | rule[@"outboundTag"] = _routeToBox.stringValue;
279 | [rule removeObjectForKey:@"balancerTag"];
280 | }
281 | }
282 | if (notification.object == _portField) {
283 | NSMutableDictionary* rule =
284 | self.routingRuleSets[_selectedRuleSet][@"rules"][_selectedRule];
285 | NSString* trimedInput = [_portField.stringValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
286 | NSNumberFormatter* f = [[NSNumberFormatter alloc] init];
287 | f.numberStyle = NSNumberFormatterDecimalStyle;
288 | NSNumber *port = [f numberFromString:trimedInput];
289 | rule[@"port"] = nilCoalescing(port, trimedInput);
290 | }
291 | if (notification.object == _inboundTagBox) {
292 | self.routingRuleSets[_selectedRuleSet][@"rules"][_selectedRule][@"inboundTag"] = _inboundTagBox.stringValue;
293 | }
294 | }
295 |
296 | - (IBAction)addRemoveRuleSet:(id)sender {
297 | [[self window] makeFirstResponder:_ruleSetTable];
298 | if ([sender selectedSegment] == 0) {
299 | NSMutableDictionary* newRuleSet = [ROUTING_DIRECT mutableDeepCopy];
300 | newRuleSet[@"name"] = @"new_rule_set";
301 | [_routingRuleSets addObject:newRuleSet];
302 | [_ruleSetTable reloadData];
303 | [_ruleSetTable selectRowIndexes:[NSIndexSet indexSetWithIndex:_routingRuleSets.count - 1] byExtendingSelection:NO]; // toggle
304 | } else if([sender selectedSegment] == 1 && _selectedRuleSet > 0 && _selectedRuleSet < _routingRuleSets.count){
305 | [_routingRuleSets removeObjectAtIndex:_selectedRuleSet];
306 | NSUInteger originalIndex = _ruleSetTable.selectedRow;
307 | [_ruleSetTable selectRowIndexes:[NSIndexSet indexSetWithIndex:MIN(_selectedRuleSet, _routingRuleSets.count - 1)] byExtendingSelection:NO]; // toggle
308 | if (originalIndex == _ruleSetTable.selectedRow) {
309 | [self tableViewSelectionDidChange:[NSNotification notificationWithName:NSTableViewSelectionDidChangeNotification object:_ruleSetTable]];
310 | }
311 | [_ruleSetTable reloadData];
312 | } else if ([sender selectedSegment] == 2) {
313 | NSAlert* alert = [[NSAlert alloc] init];
314 | alert.messageText = @"Do you want to reset rule sets to original three ones?";
315 | [alert addButtonWithTitle:@"OK"];
316 | [alert addButtonWithTitle:@"Cancel"];
317 | [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
318 | if (returnCode == NSAlertFirstButtonReturn) {
319 | NSLog(@"will rest");
320 | self->_routingRuleSets = [@[ROUTING_GLOBAL, ROUTING_BYPASSCN_PRIVATE_APPLE, ROUTING_DIRECT] mutableDeepCopy];
321 | NSUInteger originalIndex = self->_ruleSetTable.selectedRow;
322 | [self->_ruleSetTable selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO]; // toggle
323 | if (originalIndex == self->_ruleSetTable.selectedRow) {
324 | [self tableViewSelectionDidChange:[NSNotification notificationWithName:NSTableViewSelectionDidChangeNotification object:self->_ruleSetTable]];
325 | }
326 | [self->_ruleSetTable reloadData];
327 | }
328 | }];
329 | } else if ([sender selectedSegment] == 3 && _selectedRuleSet >= 0 && _selectedRuleSet < _routingRuleSets.count) {
330 | [_routingRuleSets addObject:[[_routingRuleSets objectAtIndex:_selectedRuleSet] mutableDeepCopy]];
331 | [_ruleSetTable reloadData];
332 | [_ruleSetTable selectRowIndexes:[NSIndexSet indexSetWithIndex:_routingRuleSets.count - 1] byExtendingSelection:NO]; // toggle
333 | }
334 | }
335 |
336 | - (IBAction)didSelectStrategy:(id)sender {
337 | if (_selectedRuleSet < _routingRuleSets.count) {
338 | _routingRuleSets[_selectedRuleSet][@"domainStrategy"] = _domainStrategyButton.selectedItem.title;
339 | }
340 | }
341 |
342 | - (IBAction)addRemoveRule:(id)sender {
343 | [[self window] makeFirstResponder:_ruleTable];
344 | NSMutableArray* rules = _routingRuleSets[_selectedRuleSet][@"rules"];
345 | if ([sender selectedSegment] == 0) {
346 | [rules insertObject:[@{
347 | @"type": @"field",
348 | @"outboundTag": @"direct"} mutableDeepCopy] atIndex:rules.count-1];
349 | } else {
350 | if (_selectedRule + 1 == [rules count]) {
351 | return;
352 | }
353 | [rules removeObjectAtIndex:_selectedRule];
354 | }
355 | [_ruleTable reloadData];
356 | self.selectedRule = _selectedRule; // toggle
357 | }
358 |
359 | - (IBAction)didClickEnable:(id)sender {
360 |
361 | #define enableFilter(enableButton, field, key) \
362 | field.enabled = enableButton.state; \
363 | if (!enableButton.state) { \
364 | [self.routingRuleSets[_selectedRuleSet][@"rules"][_selectedRule] removeObjectForKey:key];\
365 | }
366 |
367 | if (sender == _domainEnableButton) {
368 | enableFilter(_domainEnableButton, _editDomainButton, @"domain")
369 | if (_domainEnableButton.state) {
370 | [self showEditView:_editDomainButton];
371 | } else if (self.popover.shown) {
372 | [self.popover close];
373 | }
374 | } else if (sender == _ipEnableButton) {
375 | enableFilter(_ipEnableButton, _editIpButton, @"ip")
376 | if (_ipEnableButton.state) {
377 | [self showEditView:_editIpButton];
378 | } else if (self.popover.shown) {
379 | [self.popover close];
380 | }
381 | } else if (sender == _inboundEnableButton) {
382 | enableFilter(_inboundEnableButton, _inboundTagBox, @"inboundTag")
383 | if (_inboundEnableButton.state) {
384 | [self.routingRuleSets[_selectedRuleSet][@"rules"][_selectedRule] setObject:_inboundTagBox.stringValue forKey:@"inboundTag"];
385 | }
386 | } else if (sender == _protocolEnableButton) {
387 | enableFilter(_protocolEnableButton, _protocolButton, @"protocol")
388 | if (_protocolEnableButton.state) {
389 | [self didSelectRuleItem:_protocolButton];
390 | }
391 | } else if (sender == _networkEnableButton) {
392 | enableFilter(_networkEnableButton, _networkListButton, @"network")
393 | if (_networkEnableButton.state) {
394 | [self didSelectRuleItem:_networkListButton];
395 | }
396 | } else if (sender == _portEnableButton) {
397 | enableFilter(_portEnableButton, _portField, @"port")
398 | if (_portEnableButton.state) {
399 | [self textFieldDidChange:[[NSNotification alloc] initWithName:NSTextDidChangeNotification object:_portField userInfo:nil]];
400 | }
401 | }
402 | }
403 |
404 |
405 | - (IBAction)didSelectRuleItem:(id)sender {
406 | if (sender == _networkListButton && _networkEnableButton.state) {
407 | [self.routingRuleSets[_selectedRuleSet][@"rules"][_selectedRule] setObject:_networkListButton.selectedItem.title forKey:@"network"];
408 | } else if (sender == _protocolButton && _protocolEnableButton.state) {
409 | [self.routingRuleSets[_selectedRuleSet][@"rules"][_selectedRule] setObject:_protocolButton.selectedItem.title forKey:@"protocol"];
410 | } else if ((sender == _inboundTagBox && _inboundEnableButton.state) || sender == _routeToBox) {
411 | [self textFieldDidChange:[[NSNotification alloc] initWithName:NSTextDidChangeNotification object:sender userInfo:nil]];
412 | }
413 | }
414 |
415 | - (IBAction)showEditView:(id)sender {
416 | if (self.popover.shown) {
417 | [self.popover close];
418 | }
419 | self.popover = [[NSPopover alloc] init];
420 | self.popover.behavior = NSPopoverBehaviorApplicationDefined;
421 | self.popover.contentViewController = [[NSViewController alloc] init];
422 | if (sender == _editDomainButton) {
423 | self.popover.contentViewController.view = _domainListEditView;
424 | } else if (sender == _editIpButton) {
425 | self.popover.contentViewController.view = _ipListEditView;
426 | }
427 | [self.popover showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge];
428 | }
429 | - (IBAction)saveDomainOrIPList:(id)sender {
430 | [self.popover close];
431 | if (sender == _saveIPListButton) {
432 | self.routingRuleSets[_selectedRuleSet][@"rules"][_selectedRule][@"ip"] = [[_ipTextView string] componentsSeparatedByString:@"\n"];
433 | NSLog(@"%@", self.routingRuleSets[_selectedRuleSet][@"rules"][_selectedRule][@"ip"]);
434 | } else {
435 | self.routingRuleSets[_selectedRuleSet][@"rules"][_selectedRule][@"domain"] = [[_ipTextView string] componentsSeparatedByString:@"\n"];
436 | }
437 | }
438 |
439 | - (IBAction)closeDomainOrIPList:(id)sender {
440 | [self.popover close];
441 | }
442 |
443 |
444 | //
445 |
446 | - (IBAction)addRemoveSubscription:(id)sender {
447 | NSLog(@"%@", sender);
448 | if ([sender selectedSegment] == 0) {
449 | [_subscriptions addObject:@"enter your subscription link here"];
450 | [_subscriptionTable reloadData];
451 | } else if ([sender selectedSegment] == 1 && [_subscriptionTable selectedRow] >= 0 && [_subscriptionTable selectedRow] < _subscriptions.count) {
452 | [_subscriptions removeObjectAtIndex:[_subscriptionTable selectedRow]];
453 | [_subscriptionTable reloadData];
454 | }
455 | NSLog(@"%@", _subscriptions);
456 | }
457 |
458 | // configs
459 |
460 | - (IBAction)addRemoveConfig:(id)sender {
461 | if ([sender selectedSegment] == 0) {
462 | [_configs addObject:@"/path/to/your/config.json"];
463 | [_configTable reloadData];
464 | // [_configTable selectRowIndexes:[NSIndexSet indexSetWithIndex:[_configs count] -1] byExtendingSelection:NO];
465 | // [_configTable setFocusedColumn:[_configs count] - 1];
466 | } else if ([sender selectedSegment] == 1 && [_configTable selectedRow] >= 0 && [_configTable selectedRow] < _configs.count) {
467 | [_configs removeObjectAtIndex:[_configTable selectedRow]];
468 | [_configTable reloadData];
469 | }
470 | }
471 |
472 | - (BOOL)checkConfig {
473 | [_checkLabel setHidden:NO];
474 | NSString* v2rayBinPath = [configWindowController.appDelegate getV2rayPath];
475 | for (NSString* filePath in _configs) {
476 | int returnCode = runCommandLine(v2rayBinPath, @[@"-test", @"-config", filePath]);
477 | if (returnCode != 0) {
478 | [_checkLabel setHidden:YES];
479 | NSAlert *alert = [[NSAlert alloc] init];
480 | [alert setMessageText:[NSString stringWithFormat:@"%@ is not a valid v2ray config file", filePath]];
481 | [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
482 | return;
483 | }];
484 | return NO;
485 | }
486 | }
487 | return YES;
488 | }
489 |
490 | // core
491 | - (IBAction)showCorePath:(id)sender {
492 | if (![[NSFileManager defaultManager] fileExistsAtPath:self.corePathField.stringValue]) {
493 | [[NSFileManager defaultManager] createDirectoryAtPath:self.corePathField.stringValue withIntermediateDirectories:YES attributes:nil error:nil];
494 | }
495 | [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[[NSURL fileURLWithPath:self.corePathField.stringValue]]];
496 | }
497 |
498 | - (IBAction)changeEnableRestore:(NSPopUpButton*)sender {
499 | _enableRestore = [sender indexOfSelectedItem];
500 | }
501 |
502 | - (IBAction)showInformation:(id)sender {
503 | self.popover = [[NSPopover alloc] init];
504 | self.popover.behavior = NSPopoverBehaviorTransient;
505 | self.popover.contentViewController = [[NSViewController alloc] init];
506 | if (sender == _domainIpHelpButton) {
507 | self.popover.contentViewController.view = _domainIpHelpView;
508 | } else if (sender == _routeToHelpButton) {
509 | self.popover.contentViewController.view = _routingTagHelpView;
510 | }
511 | [self.popover showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge];
512 | }
513 |
514 | - (IBAction)showHelp:(id)sender {
515 | NSString* tabTitle = _mainTabView.selectedTabViewItem.label;
516 | // NSLog(@"%@", _mainTabView.selectedTabViewItem.label);
517 | if ([@"Rules" isEqualToString:tabTitle]) {
518 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.v2ray.com/chapter_02/03_routing.html"]];
519 | } else if ([@"Outbounds" isEqualToString:tabTitle]) {
520 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.v2ray.com/chapter_02/01_overview.html#outboundobject"]];
521 | } else if ([@"Configs" isEqualToString:tabTitle]) {
522 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.v2ray.com/chapter_02/01_overview.html"]];
523 | } else if ([@"V2Ray Core" isEqualToString:tabTitle]) {
524 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.v2ray.com/chapter_00/install.html#download"]];
525 | }
526 | }
527 |
528 |
529 | - (void)showAlert:(NSString*)text {
530 | NSAlert* alert = [[NSAlert alloc] init];
531 | [alert setInformativeText:text];
532 | [alert beginSheetModalForWindow:self.window completionHandler:nil];
533 | }
534 |
535 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
536 | // NSLog(@"keypath=%@", keyPath);
537 | // outboud
538 | if ([@"selectedOutbound" isEqualToString:keyPath]) {
539 | if (_selectedOutbound > -1) {
540 | [_outboundTable selectRowIndexes:[NSIndexSet indexSetWithIndex:_selectedOutbound] byExtendingSelection:NO];
541 | _outboundJsonView.string = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:_outbounds[_selectedOutbound] options:NSJSONWritingPrettyPrinted error:nil] encoding:NSUTF8StringEncoding];
542 | } else {
543 | _outboundJsonView.string = @"";
544 | }
545 | }
546 | // rule
547 | if ([@"selectedRuleSet" isEqualToString:keyPath] && _selectedRuleSet < _routingRuleSets.count) {
548 | _ruleSetNameField.stringValue = _routingRuleSets[_selectedRuleSet][@"name"];
549 | [_domainStrategyButton selectItemAtIndex:searchInArray(_routingRuleSets[_selectedRuleSet][@"domainStrategy"], DOMAIN_STRATEGY_LIST)];
550 | self.selectedRule = _selectedRule; //toggle
551 | }
552 | if ([@"selectedRule" isEqualToString:keyPath] && _selectedRuleSet < _routingRuleSets.count && _selectedRule < [_routingRuleSets[_selectedRuleSet][@"rules"] count] ) {
553 | if (self.popover.shown) {
554 | [self.popover close];
555 | }
556 | NSDictionary* rules = _routingRuleSets[_selectedRuleSet][@"rules"][_selectedRule];
557 | BOOL selectedLastRule = _selectedRule + 1 == [_routingRuleSets[_selectedRuleSet][@"rules"] count];
558 |
559 | _domainEnableButton.enabled = !selectedLastRule;
560 | _domainEnableButton.state = rules[@"domain"] != NULL;
561 | _editDomainButton.enabled = _domainEnableButton.state;
562 | if (rules[@"domain"]) {
563 | _domainTextView.string = [rules[@"domain"] componentsJoinedByString:@"\n"];
564 | } else {
565 | _domainTextView.string = @"";
566 | }
567 |
568 | _ipEnableButton.enabled = !selectedLastRule;
569 | _ipEnableButton.state = rules[@"ip"] != NULL;
570 | _editIpButton.enabled = _ipEnableButton.state;
571 | if (rules[@"ip"]) {
572 | _ipTextView.string = [rules[@"ip"] componentsJoinedByString:@"\n"];
573 | } else {
574 | _ipTextView.string = @"";
575 | }
576 |
577 | _inboundEnableButton.enabled = !selectedLastRule;
578 | _inboundEnableButton.state = rules[@"inboundTag"] != NULL;
579 | _inboundTagBox.enabled = _inboundEnableButton.state;
580 | _inboundTagBox.stringValue = nilCoalescing(rules[@"inboundTag"], @"");
581 |
582 | _protocolEnableButton.enabled = !selectedLastRule;
583 | _protocolEnableButton.state = rules[@"protocol"] != NULL;
584 | _protocolButton.enabled = _protocolEnableButton.state;
585 | [_protocolButton selectItemAtIndex:searchInArray(rules[@"protocol"], SNIFFING_PROTOCOL)];
586 |
587 | _portEnableButton.enabled = !selectedLastRule;
588 | _portEnableButton.state = rules[@"port"] != NULL;
589 | _portField.enabled = _portEnableButton.state && !selectedLastRule;
590 | _portField.objectValue = nilCoalescing(rules[@"port"], @"0-65535");
591 |
592 | _networkEnableButton.enabled = !selectedLastRule;
593 | _networkEnableButton.state = rules[@"network"] != NULL;
594 | _networkListButton.enabled = _networkEnableButton.state;
595 | [_networkListButton selectItemAtIndex:searchInArray(rules[@"network"], ROUTING_NETWORK_LIST)];
596 |
597 |
598 | _routeToBox.stringValue = rules[@"outboundTag"] ? rules[@"outboundTag"] : rules[@"balancerTag"];
599 | }
600 | }
601 |
602 | - (IBAction)changePassword:(id)sender {
603 | if ([[_encryptionKeyField stringValue] isEqualToString:[_encryptionKeyConfirmField stringValue]]) {
604 | if ([[_encryptionKeyField stringValue] length] > 32) {
605 | self.encryptionKey = [_encryptionKeyField.stringValue substringToIndex:32];
606 | } else {
607 | self.encryptionKey = [_encryptionKeyConfirmField.stringValue stringByPaddingToLength:32 withString:@"-" startingAtIndex:0];
608 | }
609 | [_changeIndicatorField setStringValue:@"success"];
610 | } else {
611 | [_changeIndicatorField setStringValue:@"two password do not match each other."];
612 | }
613 | }
614 |
615 | @end
616 |
--------------------------------------------------------------------------------
/V2RayX/AppDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.h
3 | // V2RayX
4 | //
5 | // Copyright © 2016年 Cenmrev. All rights reserved.
6 | //
7 |
8 |
9 | #import
10 | #import "sysconf_version.h"
11 | #import "utilities.h"
12 | #define kV2RayXHelper @"/Library/Application Support/V2RayX/v2rayx_sysconf"
13 | #define kV2RayXSettingVersion 4
14 |
15 | #define webServerPort 8070
16 |
17 | typedef enum ProxyMode : NSInteger{
18 | pacMode,
19 | globalMode,
20 | manualMode
21 | } ProxyMode;
22 |
23 |
24 | int runCommandLine(NSString* launchPath, NSArray* arguments);
25 |
26 | @interface AppDelegate : NSObject {
27 | BOOL proxyState;
28 | ProxyMode proxyMode;
29 | NSInteger localPort;
30 | NSInteger httpPort;
31 | BOOL udpSupport;
32 | BOOL shareOverLan;
33 | BOOL useCusProfile;
34 | BOOL useMultipleServer;
35 | NSInteger selectedServerIndex;
36 | NSInteger selectedCusServerIndex;
37 | NSString* selectedPacFileName;
38 | NSString* dnsString;
39 | NSMutableArray *profiles;
40 | NSMutableArray *cusProfiles;
41 | NSString* logLevel;
42 |
43 | NSString* logDirPath;
44 | }
45 |
46 | @property NSString* logDirPath;
47 |
48 | @property BOOL proxyState;
49 | @property ProxyMode proxyMode;
50 | @property NSInteger localPort;
51 | @property NSInteger httpPort;
52 | @property BOOL udpSupport;
53 | @property BOOL shareOverLan;
54 | @property BOOL useCusProfile;
55 | @property NSInteger selectedServerIndex;
56 | @property NSInteger selectedCusServerIndex;
57 | @property NSInteger selectedRoutingSet;
58 | @property NSString* dnsString;
59 | @property NSMutableArray *profiles;
60 | @property NSMutableArray *cusProfiles;
61 | @property (atomic) NSMutableArray *subsOutbounds;
62 | @property NSMutableArray *routingRuleSets;
63 | @property NSString* logLevel;
64 | @property BOOL useMultipleServer;
65 | @property NSString* selectedPacFileName;
66 | @property BOOL enableRestore;
67 | @property NSMutableArray *subscriptions;
68 | @property BOOL enableEncryption;
69 | @property NSString* encryptionKey;
70 |
71 | - (IBAction)didChangeStatus:(id)sender;
72 | - (IBAction)updateSubscriptions:(id)sender;
73 | - (IBAction)showHelp:(id)sender;
74 | - (IBAction)showConfigWindow:(id)sender;
75 | - (IBAction)editPac:(id)sender;
76 | - (IBAction)resetPac:(id)sender;
77 | - (IBAction)viewLog:(id)sender;
78 | - (void)saveConfigInfo;
79 |
80 | -(NSString*)getV2rayPath;
81 | - (NSString*)logDirPath;
82 |
83 | @property (weak) IBOutlet NSMenuItem *updateServerItem;
84 | @property (strong, nonatomic) NSStatusItem *statusBarItem;
85 | @property (weak) IBOutlet NSMenuItem *upgradeMenuItem;
86 | @property (weak, nonatomic) IBOutlet NSMenu *statusBarMenu;
87 | @property (weak, nonatomic) IBOutlet NSMenuItem *v2rayStatusItem;
88 | @property (weak, nonatomic) IBOutlet NSMenuItem *enableV2rayItem;
89 | @property (weak, nonatomic) IBOutlet NSMenuItem *pacModeItem;
90 | @property (weak, nonatomic) IBOutlet NSMenuItem *v2rayRulesItem;
91 | @property (weak) IBOutlet NSMenu *ruleSetMenuList;
92 | @property (weak, nonatomic) IBOutlet NSMenuItem *globalModeItem;
93 | @property (weak) IBOutlet NSMenuItem *manualModeItem;
94 | @property (weak, nonatomic) IBOutlet NSMenuItem *serversItem;
95 | @property (weak, nonatomic) IBOutlet NSMenu *serverListMenu;
96 | @property (weak, nonatomic) IBOutlet NSMenu *pacListMenu;
97 | @property (weak) IBOutlet NSMenuItem *editPacMenuItem;
98 |
99 | @property (weak) IBOutlet NSMenu *authMenu;
100 |
101 | @end
102 |
103 |
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "vx16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "vx32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "vx32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "vx64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "vx128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "vx256.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "vx256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "vx 512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "vx 512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "idiom" : "mac",
59 | "size" : "512x512",
60 | "scale" : "2x"
61 | }
62 | ],
63 | "info" : {
64 | "version" : 1,
65 | "author" : "xcode"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/AppIcon.appiconset/vx 512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/AppIcon.appiconset/vx 512.png
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/AppIcon.appiconset/vx128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/AppIcon.appiconset/vx128.png
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/AppIcon.appiconset/vx16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/AppIcon.appiconset/vx16.png
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/AppIcon.appiconset/vx256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/AppIcon.appiconset/vx256.png
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/AppIcon.appiconset/vx32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/AppIcon.appiconset/vx32.png
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/AppIcon.appiconset/vx64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/AppIcon.appiconset/vx64.png
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/config_duplicate.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "duplicate-document16.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "duplicate-document.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/config_duplicate.imageset/duplicate-document.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/config_duplicate.imageset/duplicate-document.png
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/config_duplicate.imageset/duplicate-document16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/config_duplicate.imageset/duplicate-document16.png
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/statusBarIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "b16.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "b32.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/statusBarIcon.imageset/b16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/statusBarIcon.imageset/b16.png
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/statusBarIcon.imageset/b32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/statusBarIcon.imageset/b32.png
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/statusBarIcon_disabled.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "g16.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "g32.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/statusBarIcon_disabled.imageset/g16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/statusBarIcon_disabled.imageset/g16.png
--------------------------------------------------------------------------------
/V2RayX/Assets.xcassets/statusBarIcon_disabled.imageset/g32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/V2RayX/Assets.xcassets/statusBarIcon_disabled.imageset/g32.png
--------------------------------------------------------------------------------
/V2RayX/ConfigImporter.h:
--------------------------------------------------------------------------------
1 | //
2 | // ConfigImporter.h
3 | // V2RayX
4 | //
5 | //
6 |
7 | #import
8 | #import "ServerProfile.h"
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface ConfigImporter : NSObject
13 | + (NSDictionary*)parseStandardSSLink:(NSString*)link;
14 | + (NSMutableDictionary*)ssOutboundFromSSLink:(NSString*)link;
15 | + (NSMutableDictionary*)ssOutboundFromSSConfig:(NSDictionary*)jsonObject;
16 | + (ServerProfile*)importFromVmessOfV2RayN:(NSString*)vmessStr;
17 | + (NSMutableDictionary*)importFromHTTPSubscription:(NSString*)httpLink;
18 | + (NSMutableDictionary*)importFromStandardConfigFiles:(NSArray*)files;
19 | + (NSMutableDictionary*)validateRuleSet:(NSMutableDictionary*)set;
20 | + (NSMutableDictionary* _Nonnull)importFromSubscriptionOfSSD: (NSString* _Nonnull)ssdLink;
21 | @end
22 |
23 | NS_ASSUME_NONNULL_END
24 |
--------------------------------------------------------------------------------
/V2RayX/ConfigImporter.m:
--------------------------------------------------------------------------------
1 | //
2 | // ConfigImporter.m
3 | // V2RayX
4 | //
5 | //
6 |
7 | #import "ConfigImporter.h"
8 | #import "utilities.h"
9 |
10 | @implementation ConfigImporter
11 |
12 | + (NSString* _Nonnull)decodeBase64String:(NSString*)encoded {
13 | if (!encoded || ![encoded isKindOfClass:[NSString class]] || encoded.length == 0) {
14 | return @"";
15 | }
16 | NSMutableString* fixed = [encoded mutableCopy];
17 | NSInteger numAdd = (4 - encoded.length % 4) % 4;
18 | for (int i = 0; i < numAdd; i += 1) {
19 | [fixed appendString:@"="];
20 | }
21 | @try {
22 | NSData* decodedData = [[NSData alloc] initWithBase64EncodedString:fixed options:NSDataBase64DecodingIgnoreUnknownCharacters];
23 | NSString* decodedString = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding];
24 | assert(decodedString != nil);
25 | return decodedString;
26 | } @catch (NSException *exception) {
27 | return @"";
28 | }
29 | }
30 |
31 | + (NSDictionary*)parseLegacySSLink:(NSString*)link {
32 | //http://shadowsocks.org/en/config/quick-guide.html
33 | @try {
34 | NSString* encoded = [[link stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] substringFromIndex:5];
35 | NSArray* hashTagSeperatedParts = [encoded componentsSeparatedByString:@"#"];
36 | NSString* encodedRemoveTag = hashTagSeperatedParts[0];
37 | NSString* decoded = [ConfigImporter decodeBase64String:encodedRemoveTag];
38 | NSArray* parts = [decoded componentsSeparatedByString:@"@"];
39 | NSArray* addressAndPort = [parts[1] componentsSeparatedByString:@":"];
40 | NSMutableArray* methodAndPassword = [[parts[0] componentsSeparatedByString:@":"] mutableCopy];
41 | NSString* method = methodAndPassword[0];
42 | [methodAndPassword removeObjectAtIndex:0];
43 |
44 | NSNumberFormatter* f = [[NSNumberFormatter alloc] init];
45 | f.numberStyle = NSNumberFormatterDecimalStyle;
46 | NSNumber *port = [f numberFromString:addressAndPort[1]];
47 |
48 | if (hashTagSeperatedParts.count == 1) {
49 | return @{
50 | @"server":addressAndPort[0],
51 | @"server_port":port,
52 | @"password": [methodAndPassword componentsJoinedByString:@":"],
53 | @"method":method};
54 | } else {
55 | return @{
56 | @"server":addressAndPort[0],
57 | @"server_port":port,
58 | @"password": [methodAndPassword componentsJoinedByString:@":"],
59 | @"method":method,
60 | @"tag":hashTagSeperatedParts[1]
61 | };
62 | }
63 | } @catch (NSException *exception) {
64 | return nil;
65 | }
66 | }
67 |
68 |
69 | + (NSDictionary*)parseStandardSSLink:(NSString*)link {
70 | //https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html
71 | if (![@"ss://" isEqualToString: [link substringToIndex:5]]) {
72 | return nil;
73 | }
74 | @try {
75 | NSURL* ssurl = [[NSURL alloc] initWithString:link];
76 | NSNumber* enableOTA;
77 | if ([@"ota=false" isEqualToString:[ssurl.query lowercaseString]]) {
78 | enableOTA = @(NO);
79 | } else if ([@"ota=true" isEqualToString:[ssurl.query lowercaseString]]) {
80 | enableOTA = @(YES);
81 | } else if (ssurl.query.length > 0) {
82 | return nil; // only support ota
83 | }
84 | NSString* userinfoDecoded = [ConfigImporter decodeBase64String:ssurl.user];
85 | NSArray* userinfo = [userinfoDecoded componentsSeparatedByString:@":"];
86 | if(enableOTA == nil) {
87 | return @{
88 | @"server":ssurl.host,
89 | @"server_port":ssurl.port,
90 | @"password": userinfo[1],
91 | @"method":userinfo[0],
92 | @"tag":ssurl.fragment
93 | };
94 | } else {
95 | return @{
96 | @"server":ssurl.host,
97 | @"server_port":ssurl.port,
98 | @"password": userinfo[1],
99 | @"method":userinfo[0],
100 | @"tag":ssurl.fragment,
101 | @"ota":enableOTA
102 | };
103 | }
104 | } @catch (NSException *exception) {
105 | NSLog(@"%@", exception);
106 | return nil;
107 | }
108 | }
109 |
110 | + (NSMutableDictionary* )ssOutboundFromSSLink:(NSString*)link {
111 | NSDictionary* parsed = [ConfigImporter parseStandardSSLink:link];
112 | if (parsed) {
113 | return [ConfigImporter ssOutboundFromSSConfig:parsed];
114 | } else {
115 | parsed = [ConfigImporter parseLegacySSLink:link];
116 | if (parsed) {
117 | return [ConfigImporter ssOutboundFromSSConfig:parsed];
118 | }
119 | }
120 | return nil;
121 | }
122 |
123 | + (NSMutableDictionary*)ssOutboundFromSSConfig:(NSDictionary*)jsonObject {
124 | if (jsonObject && jsonObject[@"server"] && jsonObject[@"server_port"] && jsonObject[@"password"] && jsonObject[@"method"] && [SUPPORTED_SS_SECURITY indexOfObject:jsonObject[@"method"]] != NSNotFound) {
125 | NSMutableDictionary* ssOutbound =
126 | [@{
127 | @"sendThrough": @"0.0.0.0",
128 | @"protocol": @"shadowsocks",
129 | @"settings": @{
130 | @"servers": @[
131 | @{
132 | @"address": jsonObject[@"server"],
133 | @"port": jsonObject[@"server_port"],
134 | @"method": jsonObject[@"method"],
135 | @"password": jsonObject[@"password"],
136 | }
137 | ]
138 | },
139 | @"tag": [NSString stringWithFormat:@"%@:%@",jsonObject[@"server"],jsonObject[@"server_port"]],
140 | @"streamSettings": @{},
141 | @"mux": @{}
142 | } mutableDeepCopy];
143 | if (jsonObject[@"ota"]) {
144 | ssOutbound[@"settings"][@"servers"][0][@"ota"] = jsonObject[@"ota"];
145 | }
146 | if (jsonObject[@"tag"] && [jsonObject[@"tag"] isKindOfClass:[NSString class]] && [jsonObject[@"tag"] length] ) {
147 | ssOutbound[@"tag"] = jsonObject[@"tag"];
148 | }
149 | if ([jsonObject[@"fast_open"] isKindOfClass:[NSNumber class]]) {
150 | ssOutbound[@"streamSettings"] =[@{ @"sockopt": @{
151 | @"tcpFastOpen": jsonObject[@"fast_open"]
152 | }} mutableDeepCopy];
153 | }
154 | return ssOutbound;
155 | }
156 | return nil;
157 | }
158 |
159 | + (NSMutableDictionary*)validateRuleSet:(NSMutableDictionary*)set {
160 | if (![set isKindOfClass:[NSMutableDictionary class]]) {
161 | NSLog(@"not a mutable dictionary class, %@", [set className]);
162 | return nil;
163 | }
164 | if (!set[@"rules"] || ![set[@"rules"] isKindOfClass:[NSMutableArray class]] || ![set count] ) {
165 | NSLog(@"no rules");
166 | return nil;
167 | }
168 | if (![@"0-65535" isEqualToString: [set[@"rules"] lastObject][@"port"]]) {
169 | NSMutableDictionary *lastRule = [@{
170 | @"type" : @"field",
171 | @"outboundTag" : @"main",
172 | @"port" : @"0-65535"
173 | } mutableDeepCopy];
174 | [set[@"rules"] addObject:lastRule];
175 | }
176 | NSMutableArray* ruleToRemove = [[NSMutableArray alloc] init];
177 | NSArray* notSupported = NOT_SUPPORTED_ROUTING;
178 | NSArray* supported = SUPPORTED_ROUTING;
179 | // currently, source/user/inboundTag/protocol are not supported
180 | for (NSMutableDictionary* aRule in set[@"rules"]) {
181 | [aRule removeObjectsForKeys:notSupported];
182 | BOOL shouldRemove = true;
183 | for (NSString* supportedKey in supported) {
184 | if (aRule[supportedKey]) {
185 | shouldRemove = false;
186 | break;
187 | }
188 | }
189 | if (shouldRemove) {
190 | [ruleToRemove addObject:aRule];
191 | continue;
192 | }
193 | aRule[@"type"] = @"field";
194 | if (!aRule[@"outboundTag"] && !aRule[@"balancerTag"]) {
195 | aRule[@"outboundTag"] = @"main";
196 | }
197 | if (aRule[@"outboundTag"] && aRule[@"balancerTag"]) {
198 | [aRule removeObjectForKey:@"balancerTag"];
199 | }
200 | }
201 | for (NSMutableDictionary* aRule in ruleToRemove) {
202 | [set[@"rules"] removeObject:aRule];
203 | }
204 | if (!set[@"name"]) {
205 | set[@"name"] = @"some rule set";
206 | }
207 | return set;
208 | }
209 |
210 | + (NSMutableDictionary*)importFromStandardConfigFiles:(NSArray*)files {
211 | NSMutableDictionary* result = [@{@"vmess": @[], @"other": @[], @"rules":@[]} mutableDeepCopy];
212 | for (NSURL* file in files) {
213 | NSError* error;
214 | id jsonObject = [NSJSONSerialization JSONObjectWithData:
215 | [NSData dataWithContentsOfURL:file] options:0 error:&error];
216 | if (error) continue;
217 | if (![jsonObject isKindOfClass:[NSDictionary class]]) continue;
218 | NSMutableArray* outboundJSONs = [[NSMutableArray alloc] init];
219 | NSMutableArray* routingJSONs = [[NSMutableArray alloc] init];
220 | if ([[jsonObject objectForKey:@"outbound"] isKindOfClass:[NSDictionary class]]) {
221 | [outboundJSONs addObject:jsonObject[@"outbound"]];
222 | }
223 | if ([[jsonObject objectForKey:@"outboundDetour"] isKindOfClass:[NSArray class]]) {
224 | [outboundJSONs addObjectsFromArray:jsonObject[@"outboundDetour"]];
225 | }
226 | if ([[jsonObject objectForKey:@"outbounds"] isKindOfClass:[NSArray class]]) {
227 | [outboundJSONs addObjectsFromArray:jsonObject[@"outbounds"]];
228 | }
229 | for (NSDictionary* outboundJSON in outboundJSONs) {
230 | NSString* protocol = outboundJSON[@"protocol"];
231 | if (!protocol) {
232 | continue;
233 | }
234 | if ([@"vmess" isEqualToString:outboundJSON[@"protocol"]]) {
235 | [result[@"vmess"] addObject:[ServerProfile profilesFromJson:outboundJSON][0]];
236 | } else {
237 | [result[@"other"] addObject:outboundJSON];
238 | }
239 | }
240 | if ([[jsonObject objectForKey:@"routing"] isKindOfClass:[NSDictionary class]]) {
241 | [routingJSONs addObject:[jsonObject objectForKey:@"routing"]];
242 | }
243 | if ([[jsonObject objectForKey:@"routings"] isKindOfClass:[NSArray class]]) {
244 | [routingJSONs addObjectsFromArray:[jsonObject objectForKey:@"routings"]];
245 | }
246 | for (NSDictionary* routingSet in routingJSONs) {
247 | NSMutableDictionary* set = [routingSet mutableDeepCopy];
248 | if (set[@"settings"]) { // compatibal with previous config file format
249 | set = set[@"settings"];
250 | }
251 | NSMutableDictionary* validatedSet = [ConfigImporter validateRuleSet:set];
252 | if (validatedSet) {
253 | [result[@"rules"] addObject:validatedSet];
254 | }
255 | }
256 | if (jsonObject[@"server"] && jsonObject[@"server_port"] && jsonObject[@"password"] && jsonObject[@"method"] && [SUPPORTED_SS_SECURITY indexOfObject:jsonObject[@"method"]] != NSNotFound) {
257 | NSMutableDictionary* ssOutbound = [@{
258 | @"sendThrough": @"0.0.0.0",
259 | @"protocol": @"shadowsocks",
260 | @"settings": @{
261 | @"servers": @[
262 | @{
263 | @"address": jsonObject[@"server"],
264 | @"port": jsonObject[@"server_port"],
265 | @"method": jsonObject[@"method"],
266 | @"password": jsonObject[@"password"],
267 | }
268 | ]
269 | },
270 | @"tag": [NSString stringWithFormat:@"%@:%@",jsonObject[@"server"],jsonObject[@"server_port"]],
271 | @"streamSettings": @{},
272 | @"mux": @{}
273 | } mutableDeepCopy];
274 | if ([jsonObject[@"fast_open"] isKindOfClass:[NSNumber class]]) {
275 | ssOutbound[@"streamSettings"] =[@{ @"sockopt": @{
276 | @"tcpFastOpen": jsonObject[@"fast_open"]
277 | }} mutableDeepCopy];
278 | }
279 | [result[@"other"] addObject:ssOutbound];
280 | }
281 | }
282 | return result;
283 | }
284 |
285 | + (NSMutableDictionary*)importFromHTTPSubscription: (NSString*)httpLink {
286 | // https://blog.csdn.net/yi_zz32/article/details/48769487
287 | NSMutableDictionary* result = [@{@"vmess": @[], @"other": @[]} mutableDeepCopy];
288 | if ([httpLink length] < 4) {
289 | return nil;
290 | }
291 | if (![@"http" isEqualToString:[httpLink substringToIndex:4]]) {
292 | return nil;
293 | }
294 | NSURL *url = [NSURL URLWithString:httpLink];
295 | NSError *urlError = nil;
296 | NSString *urlStr = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&urlError];
297 | if (!urlError) {
298 | NSString *decodedDataStr = [ConfigImporter decodeBase64String:urlStr];
299 | if ([decodedDataStr length] == 0) {
300 | return nil;
301 | }
302 | decodedDataStr = [decodedDataStr stringByReplacingOccurrencesOfString:@"\r" withString:@""];
303 | NSArray *decodedDataArray = [decodedDataStr componentsSeparatedByString:@"\n"];
304 | for (id linkStr in decodedDataArray) {
305 | if ([linkStr length] != 0) {
306 | ServerProfile* p = [ConfigImporter importFromVmessOfV2RayN:linkStr];
307 | if (p) {
308 | [result[@"vmess"] addObject:p];
309 | continue;
310 | }
311 | NSMutableDictionary* outbound = [ConfigImporter ssOutboundFromSSLink:linkStr];
312 | if (outbound) {
313 | [result[@"other"] addObject:outbound];
314 | continue;
315 | }
316 | NSMutableDictionary* ssdResults = [ConfigImporter importFromSubscriptionOfSSD:linkStr];
317 | [result[@"other"] addObjectsFromArray:ssdResults[@"other"]];
318 | }
319 | }
320 | return result;
321 | }
322 | return nil;
323 | }
324 |
325 | + (ServerProfile*)importFromVmessOfV2RayN:(NSString*)vmessStr {
326 | if ([vmessStr length] < 9 || ![[[vmessStr substringToIndex:8] lowercaseString] isEqualToString:@"vmess://"]) {
327 | return nil;
328 | }
329 | NSString* decodedJsonString = [[ConfigImporter decodeBase64String:[vmessStr substringFromIndex:8]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
330 | NSData* decodedData = [decodedJsonString dataUsingEncoding:NSUTF8StringEncoding];
331 | NSError* jsonParseError;
332 | NSDictionary *sharedServer = [NSJSONSerialization JSONObjectWithData:
333 | decodedData options:0 error:&jsonParseError];
334 | if (jsonParseError) {
335 | return nil;
336 | }
337 | ServerProfile* newProfile = [[ServerProfile alloc] init];
338 | newProfile.outboundTag = nilCoalescing([sharedServer objectForKey:@"ps"], @"imported From QR");
339 | newProfile.address = nilCoalescing([sharedServer objectForKey:@"add"], @"");
340 | newProfile.port = [nilCoalescing([sharedServer objectForKey:@"port"], @0) intValue];
341 | newProfile.userId = nilCoalescing([sharedServer objectForKey:@"id"], newProfile.userId);
342 | newProfile.alterId = [nilCoalescing([sharedServer objectForKey:@"aid"], @0) intValue];
343 | NSDictionary *netWorkDict = @{@"tcp": @0, @"kcp": @1, @"ws":@2, @"h2":@3 };
344 | if ([sharedServer objectForKey:@"net"] && [netWorkDict objectForKey:[sharedServer objectForKey:@"net"]]) {
345 | newProfile.network = [netWorkDict[sharedServer[@"net"]] intValue];
346 | }
347 | NSMutableDictionary* streamSettings = [newProfile.streamSettings mutableDeepCopy];
348 | switch (newProfile.network) {
349 | case tcp:
350 | if (![sharedServer objectForKey:@"type"] || !([sharedServer[@"type"] isEqualToString:@"none"] || [sharedServer[@"type"] isEqualToString:@"http"])) {
351 | break;
352 | }
353 | streamSettings[@"tcpSettings"][@"header"][@"type"] = sharedServer[@"type"];
354 | if ([streamSettings[@"tcpSettings"][@"header"][@"type"] isEqualToString:@"http"]) {
355 | if ([sharedServer objectForKey:@"host"]) {
356 | streamSettings[@"tcpSettings"][@"header"][@"host"] = [sharedServer[@"host"] componentsSeparatedByString:@","];
357 | }
358 | }
359 | break;
360 | case kcp:
361 | if (![sharedServer objectForKey:@"type"]) {
362 | break;
363 | }
364 | if (![@{@"none": @0, @"srtp": @1, @"utp": @2, @"wechat-video":@3, @"dtls":@4, @"wireguard":@5} objectForKey:sharedServer[@"type"]]) {
365 | break;
366 | }
367 | streamSettings[@"kcpSettings"][@"header"][@"type"] = sharedServer[@"type"];
368 | break;
369 | case ws:
370 | if ([[sharedServer objectForKey:@"host"] containsString:@";"]) {
371 | NSArray *tempPathHostArray = [[sharedServer objectForKey:@"host"] componentsSeparatedByString:@";"];
372 | streamSettings[@"wsSettings"][@"path"] = tempPathHostArray[0];
373 | streamSettings[@"wsSettings"][@"headers"][@"Host"] = tempPathHostArray[1];
374 | }
375 | else {
376 | streamSettings[@"wsSettings"][@"path"] = nilCoalescing([sharedServer objectForKey:@"path"], @"");
377 | streamSettings[@"wsSettings"][@"headers"][@"Host"] = nilCoalescing([sharedServer objectForKey:@"host"], @"");
378 | }
379 | break;
380 | case http:
381 | if ([[sharedServer objectForKey:@"host"] containsString:@";"]) {
382 | NSArray *tempPathHostArray = [[sharedServer objectForKey:@"host"] componentsSeparatedByString:@";"];
383 | streamSettings[@"wsSettings"][@"path"] = tempPathHostArray[0];
384 | streamSettings[@"wsSettings"][@"headers"][@"Host"] = [tempPathHostArray[1] componentsSeparatedByString:@","];
385 | }
386 | else {
387 | streamSettings[@"httpSettings"][@"path"] = nilCoalescing([sharedServer objectForKey:@"path"], @"");
388 | if (![sharedServer objectForKey:@"host"]) {
389 | break;
390 | };
391 | if ([[sharedServer objectForKey:@"host"] length] > 0) {
392 | streamSettings[@"httpSettings"][@"host"] = [[sharedServer objectForKey:@"host"] componentsSeparatedByString:@","];
393 | }
394 | }
395 | break;
396 | default:
397 | break;
398 | }
399 | if ([sharedServer objectForKey:@"tls"] && [sharedServer[@"tls"] isEqualToString:@"tls"]) {
400 | streamSettings[@"security"] = @"tls";
401 | streamSettings[@"tlsSettings"][@"serverName"] = newProfile.address;
402 | }
403 | newProfile.streamSettings = streamSettings;
404 | return newProfile;
405 | }
406 |
407 | // https://github.com/CGDF-Github/SSD-Windows/wiki/订阅链接协定
408 | + (NSMutableDictionary* _Nonnull)importFromSubscriptionOfSSD: (NSString* _Nonnull)ssdLink {
409 | NSMutableDictionary* result = EMPTY_IMPORT_RESULT;
410 | @try {
411 | NSString* encodedPart;
412 | if (![[ssdLink substringToIndex:6] isEqualToString:@"ssd://"]) {
413 | return EMPTY_IMPORT_RESULT;
414 | }
415 | encodedPart = [ssdLink substringFromIndex:6];
416 | NSString* decodedJSONStr = [ConfigImporter decodeBase64String:encodedPart];
417 | NSDictionary* decodedObject = [NSJSONSerialization JSONObjectWithData:[decodedJSONStr dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
418 | NSString* airportName = decodedObject[@"airport"];
419 | NSNumber* defaultPort = decodedObject[@"port"];
420 | NSString* defaultEncryption = decodedObject[@"encryption"];
421 | NSString* defaultPassword = decodedObject[@"password"];
422 | NSString* defaultPlutgin = decodedObject[@"plugin"];
423 | NSInteger count = 0;
424 | for (NSDictionary* server in decodedObject[@"servers"]) {
425 | NSString* address = server[@"server"];
426 | NSNumber* port = nilCoalescing(server[@"port"], defaultPort) ;
427 | NSString* method = nilCoalescing(server[@"encryption"], defaultEncryption);
428 | NSString* password = nilCoalescing(server[@"password"], defaultPassword);
429 | NSString* plugIn = nilCoalescing(server[@"plugin"], defaultPlutgin);
430 | if (plugIn && plugIn.length > 0) {
431 | continue; // do not support plug-in yet
432 | }
433 | NSString* defaultServerName = [NSString stringWithFormat:@"%lu", count];
434 | NSString* serverName = nilCoalescing(server[@"remarks"], defaultServerName);
435 | NSString* tag = [NSString stringWithFormat:@"%@-%@", airportName, serverName];
436 | NSMutableDictionary* ssOutbound =
437 | [ConfigImporter ssOutboundFromSSConfig:@{ @"server":address,
438 | @"server_port":port,
439 | @"password":password,
440 | @"method":method,
441 | @"tag": tag
442 | }];
443 | if (ssOutbound) {
444 | [result[@"other"] addObject:ssOutbound];
445 | count += 1;
446 | }
447 | }
448 | NSLog(@"%@", result);
449 | return result;
450 | } @catch (NSException *exception) {
451 | return EMPTY_IMPORT_RESULT;
452 | }
453 | }
454 |
455 | @end
456 |
--------------------------------------------------------------------------------
/V2RayX/ConfigWindowController.h:
--------------------------------------------------------------------------------
1 | //
2 | // ConfigWindowController.h
3 | // V2RayX
4 | //
5 | // Copyright © 2016年 Cenmrev. All rights reserved.
6 | //
7 |
8 | #import
9 | #import "ServerProfile.h"
10 |
11 | @interface ConfigWindowController : NSWindowController
12 | - (IBAction)chooseNetwork:(NSPopUpButton *)sender;
13 | @property (weak) IBOutlet NSPopUpButton *networkButton;
14 | @property (weak) IBOutlet NSPopUpButton *vmessSecurityButton;
15 | - (IBAction)addRemoveServer:(id)sender;
16 | - (IBAction)cancel:(id)sender;
17 | - (IBAction)okSave:(id)sender;
18 | @property (weak) IBOutlet NSButton *importButton;
19 | @property (weak) IBOutlet NSTableView *profileTable;
20 | @property (weak) IBOutlet NSSegmentedControl *addRemoveButton;
21 | @property (weak) IBOutlet NSView *importResultView;
22 | @property (weak) IBOutlet NSTextField *importMessageField;
23 |
24 | @property (weak) IBOutlet NSTextField *localPortField;
25 | @property (weak) IBOutlet NSTextField *httpPortField;
26 | @property (weak) IBOutlet NSTextField *portField;
27 | @property (weak) IBOutlet NSTextField *alterIdField;
28 | @property (weak) IBOutlet NSTextField *dnsField;
29 | @property (weak) IBOutlet NSButton *globalTransportButton;
30 | @property (weak) IBOutlet NSPopUpButton *logLevelButton;
31 | @property (weak) IBOutlet NSMenu *importFromJsonMenu;
32 | @property (weak) IBOutlet NSButton *transportSettingsButton;
33 |
34 | @property (weak) AppDelegate* appDelegate;
35 | @property (nonatomic) ServerProfile* selectedProfile;
36 | @property (nonatomic) NSInteger logLevel;
37 | @property (nonatomic) NSInteger selectedServerIndex;
38 | @property (nonatomic) NSInteger selectedCusServerIndex;
39 | @property (nonatomic) NSInteger localPort;
40 | @property (nonatomic) NSInteger httpPort;
41 | @property (nonatomic) BOOL udpSupport;
42 | @property (nonatomic) BOOL shareOverLan;
43 | @property (nonatomic) NSString* dnsString;
44 | @property (nonatomic) NSMutableArray *profiles;
45 | @property (nonatomic) NSMutableArray *outbounds; // except than vmess
46 | @property (nonatomic) NSMutableArray *subscriptions;
47 | @property NSMutableArray* routingRuleSets;
48 | @property (nonatomic) NSMutableArray *cusProfiles;
49 | @property BOOL enableRestore;
50 | @property BOOL enableEncryption;
51 | @property NSString* encryptionKey;
52 |
53 | @property (weak) IBOutlet NSTextField *versionField;
54 |
55 | @end
56 |
--------------------------------------------------------------------------------
/V2RayX/ConfigWindowController.m:
--------------------------------------------------------------------------------
1 | //
2 | // ConfigWindowController.m
3 | // V2RayX
4 | //
5 | // Copyright © 2016年 Cenmrev. All rights reserved.
6 | //
7 |
8 | #import "ConfigWindowController.h"
9 | #import "AppDelegate.h"
10 | #import "MutableDeepCopying.h"
11 | #import "TransportWindowController.h"
12 | #import "AdvancedWindowController.h"
13 | #import "ConfigImporter.h"
14 |
15 | @interface ConfigWindowController ()
16 |
17 | @property (strong) TransportWindowController* transportWindowController;
18 | @property (strong) AdvancedWindowController* advancedWindowController;
19 | @property (strong) NSPopover* popover;
20 | @end
21 |
22 | @implementation ConfigWindowController
23 |
24 | - (void)windowDidLoad {
25 | [super windowDidLoad];
26 |
27 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
28 | NSString* v2rayPath = [self.appDelegate getV2rayPath];
29 |
30 | NSTask *task = [[NSTask alloc] init];
31 | if (@available(macOS 10.13, *)) {
32 | [task setExecutableURL:[NSURL fileURLWithPath:v2rayPath]];
33 | } else {
34 | [task setLaunchPath:v2rayPath];
35 | }
36 | [task setArguments:@[@"-version"]];
37 | NSPipe *stdoutpipe = [NSPipe pipe];
38 | [task setStandardOutput:stdoutpipe];
39 | [task launch];
40 | [task waitUntilExit];
41 | NSFileHandle *file = [stdoutpipe fileHandleForReading];
42 | NSData *data = [file readDataToEndOfFile];
43 | NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
44 | dispatch_async(dispatch_get_main_queue(), ^{
45 | [self->_versionField setStringValue:[string componentsSeparatedByString:@"\n"][0]];
46 | });
47 |
48 | });
49 |
50 | [_networkButton removeAllItems];
51 | for(NSString* network in NETWORK_LIST) {
52 | [_networkButton addItemWithTitle:network];
53 | }
54 | [_vmessSecurityButton removeAllItems];
55 | for(NSString* security in VMESS_SECURITY_LIST) {
56 | [_vmessSecurityButton addItemWithTitle:security];
57 | }
58 |
59 | //set textField Display
60 | [_profileTable setFocusRingType:NSFocusRingTypeNone];
61 | NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
62 | [formatter setNumberStyle:NSNumberFormatterNoStyle];
63 | [_portField setFormatter:formatter];
64 | [_alterIdField setFormatter:formatter];
65 | [_localPortField setFormatter:formatter];
66 | [_httpPortField setFormatter:formatter];
67 | // [_addRemoveButton setMenu:_importFromJsonMenu forSegment:2];
68 |
69 | // copy data
70 | _profiles = [[NSMutableArray alloc] init];
71 | _outbounds = [[NSMutableArray alloc] init];
72 | for (NSDictionary *p in _appDelegate.profiles) {
73 | if ([@"vmess" isEqualToString:p[@"protocol"]] && [p[@"settings"][@"vnext"] count] == 1) {
74 | [_profiles addObject:[ServerProfile profilesFromJson:p][0]];
75 | } else {
76 | [_outbounds addObject:p];
77 | }
78 | }
79 | _cusProfiles = [_appDelegate.cusProfiles mutableDeepCopy];
80 | _subscriptions = [_appDelegate.subscriptions mutableCopy];
81 |
82 | _routingRuleSets = [_appDelegate.routingRuleSets mutableCopy];
83 | //
84 | [_profileTable reloadData];
85 | self.selectedServerIndex = 0;
86 | self.selectedCusServerIndex = 0;
87 | self.httpPort = _appDelegate.httpPort;
88 | self.localPort = _appDelegate.localPort;
89 | self.udpSupport = _appDelegate.udpSupport;
90 | self.shareOverLan = _appDelegate.shareOverLan;
91 | self.dnsString = _appDelegate.dnsString;
92 | self.enableRestore = _appDelegate.enableRestore;
93 | self.enableEncryption = _appDelegate.enableEncryption;
94 | self.encryptionKey = [NSString stringWithString:_appDelegate.encryptionKey];
95 | NSDictionary *logLevelDic = @{
96 | @"debug": @4,
97 | @"info": @3,
98 | @"warning": @2,
99 | @"error":@1,
100 | @"none":@0
101 | };
102 | self.logLevel = [logLevelDic[_appDelegate.logLevel] integerValue];
103 |
104 | [_profileTable selectRowIndexes:[NSIndexSet indexSetWithIndex:self.selectedServerIndex] byExtendingSelection:NO];
105 | [[self window] makeFirstResponder:_profileTable];
106 | }
107 |
108 | // set controller as profilesTable's datasource
109 | - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
110 | if (tableView == _profileTable) {
111 | return [_profiles count];
112 | }
113 | return 0;
114 | }
115 |
116 | - (id) tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
117 | if (tableView == _profileTable) {
118 | if ([_profiles count] > 0) {
119 | ServerProfile* p = [_profiles objectAtIndex:row];
120 | return [[p outboundTag] length] > 0 ? [p outboundTag] : [NSString stringWithFormat:@"%@:%ld", [p address], [p port]];
121 | } else {
122 | return nil;
123 | }
124 | }
125 | return nil;
126 | }
127 |
128 | -(void)tableViewSelectionDidChange:(NSNotification *)notification{
129 | if ([notification object] == _profileTable) {
130 | if ([_profiles count] > 0) {
131 | [self setSelectedServerIndex:[_profileTable selectedRow]];
132 | // NSLog(@"selectef p = %@", _profiles[_selectedServerIndex]);
133 | [self setSelectedProfile:_profiles[_selectedServerIndex]];
134 | }
135 | }
136 | }
137 |
138 | - (IBAction)chooseNetwork:(NSPopUpButton *)sender {
139 | if (_selectedServerIndex >= 0 && _selectedServerIndex < [_profiles count]) {
140 | [self checkTLSforHttp2];
141 | }
142 | }
143 |
144 | - (BOOL)checkTLSforHttp2 {
145 | if ([_networkButton indexOfSelectedItem] == 3) { // selected http/2
146 | BOOL tlsEnabled = [self.selectedProfile.streamSettings[@"security"] isEqual: @"tls"];
147 | if (!tlsEnabled) {
148 | NSAlert *httpTlsAlerm = [[NSAlert alloc] init];
149 | [httpTlsAlerm addButtonWithTitle:@"Close"];
150 | [httpTlsAlerm addButtonWithTitle:@"Help"];
151 | [httpTlsAlerm setMessageText:@"Both client and server must enable TLS to use HTTP/2 network! Enbale TLS in transport settings. Click \"Help\" if you need more information"];
152 | [httpTlsAlerm beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
153 | if (returnCode == NSAlertSecondButtonReturn) {
154 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.v2ray.com/chapter_02/transport/h2.html#tips"]];
155 | }
156 | }];
157 | [_networkButton selectItemAtIndex:0];
158 | return NO; // does not pass checking
159 | }
160 | }
161 | return true; // pass checking
162 | }
163 |
164 | - (IBAction)addRemoveServer:(id)sender {
165 | [[self window] makeFirstResponder:_profileTable];
166 | if ([sender selectedSegment] == 0) {
167 | ServerProfile* newProfile = [[ServerProfile alloc] init];
168 | [_profiles addObject:newProfile];
169 | [_profileTable reloadData];
170 | [_profileTable selectRowIndexes:[NSIndexSet indexSetWithIndex:([_profiles count] - 1)] byExtendingSelection:NO];
171 | } else if ([sender selectedSegment] == 1 && [_profiles count] > 0) {
172 | NSInteger originalSelectedServerIndex = [_profileTable selectedRow];
173 | [_profiles removeObjectAtIndex:originalSelectedServerIndex];
174 | if ([_profiles count] > 0) {
175 | if (originalSelectedServerIndex == [_profiles count]) {//deleted the last server
176 | //select the last server of the remains
177 | [self setSelectedServerIndex:[_profiles count] - 1];
178 | }
179 | [_profileTable selectRowIndexes:[NSIndexSet indexSetWithIndex:_selectedServerIndex] byExtendingSelection:NO];
180 | [self setSelectedProfile:_profiles[_selectedServerIndex]];
181 | } else { // all the profiles are deleted;
182 | [self setSelectedServerIndex:-1];
183 | [self setSelectedProfile:nil];
184 | }
185 | [_profileTable reloadData];
186 | }
187 | else if ([sender selectedSegment] == 2) {
188 | // share server
189 | } else if ([sender selectedSegment] == 3) {
190 | // duplicate
191 | if (_selectedServerIndex >= 0 && _selectedServerIndex < [_profiles count]) {
192 | [_profiles addObject:[_profiles[_selectedServerIndex] deepCopy]];
193 | [_profileTable reloadData];
194 | }
195 | }
196 | }
197 |
198 | - (IBAction)importConfigs:(id)sender {
199 | [NSMenu popUpContextMenu:_importFromJsonMenu withEvent:[NSApp currentEvent] forView:sender];
200 | }
201 |
202 | - (IBAction)cancel:(id)sender {
203 | [[self window] close];
204 | }
205 |
206 | - (NSString*)firstFewLines:(NSDictionary*)dict {
207 | NSInteger limit = 13;
208 | NSString* str = [dict description];
209 | NSArray* lines = [str componentsSeparatedByString:@"\n"];
210 | if (lines.count < limit) {
211 | return str;
212 | } else {
213 | NSString* r = [[lines subarrayWithRange:NSMakeRange(0, limit)] componentsJoinedByString:@"\n"];
214 | return [NSString stringWithFormat:@"%@\n...", r];
215 | }
216 | }
217 |
218 | - (IBAction)okSave:(id)sender {
219 | if (![self checkTLSforHttp2]) {
220 | return;
221 | }
222 | NSMutableArray *allOutbounds = [[NSMutableArray alloc] init];
223 | for (ServerProfile* p in _profiles) {
224 | [allOutbounds addObject:[p outboundProfile]];
225 | }
226 | for (NSMutableDictionary* p in _outbounds) {
227 | [allOutbounds addObject:p];
228 | }
229 | NSMutableDictionary *allOutboundDict =[[NSMutableDictionary alloc] init];
230 | for (NSMutableDictionary* outbound in allOutbounds) {
231 | if (!outbound[@"tag"] || ![outbound[@"tag"] isKindOfClass:[NSString class]] || [outbound[@"tag"] length] == 0) {
232 | [self showAlert: [NSString stringWithFormat:@"%@\ntag is not valid!", [self firstFewLines:outbound]]];
233 | return;
234 | }
235 | if ([RESERVED_TAGS indexOfObject:outbound[@"tag"]] != NSNotFound) {
236 | [self showAlert: [NSString stringWithFormat:@"tag %@ is reserved, please use another one!", outbound[@"tag"]]];
237 | return;
238 | }
239 | if (allOutboundDict[outbound[@"tag"]]) {
240 | [self showAlert: [NSString stringWithFormat:@"The two outbounds share the same tag: %@\n%@\nAND\n%@",outbound[@"tag"], [self firstFewLines:outbound], [self firstFewLines:allOutboundDict[outbound[@"tag"]]]]];
241 | return;
242 | }
243 | allOutboundDict[outbound[@"tag"]] = outbound;
244 | }
245 | _appDelegate.profiles = allOutbounds;
246 | NSString* dnsStr = [[_dnsField stringValue] stringByReplacingOccurrencesOfString:@" " withString:@""];
247 | if ([dnsStr length] == 0) {
248 | dnsStr = @"localhost";
249 | }
250 | _appDelegate.logLevel = _logLevelButton.selectedItem.title;
251 | _appDelegate.localPort = [_localPortField integerValue];
252 | _appDelegate.httpPort = [_httpPortField integerValue];
253 | _appDelegate.udpSupport = self.udpSupport;
254 | _appDelegate.shareOverLan = self.shareOverLan;
255 | _appDelegate.dnsString = dnsStr;
256 | _appDelegate.cusProfiles = self.cusProfiles;
257 | _appDelegate.subscriptions = self.subscriptions;
258 | _appDelegate.enableRestore = self.enableRestore;
259 | [_appDelegate.routingRuleSets removeAllObjects];
260 | for (NSMutableDictionary* set in self.routingRuleSets) {
261 | NSMutableDictionary* validatedSet = [ConfigImporter validateRuleSet:set];
262 | if (validatedSet) {
263 | [_appDelegate.routingRuleSets addObject:validatedSet];
264 | }
265 | }
266 | if (_appDelegate.routingRuleSets.count == 0) {
267 | [_appDelegate.routingRuleSets addObject:[ROUTING_DIRECT mutableDeepCopy]];
268 | }
269 | _appDelegate.enableEncryption = self.enableEncryption;
270 | _appDelegate.encryptionKey = self.encryptionKey;
271 | [_appDelegate saveConfigInfo];
272 | [_appDelegate updateSubscriptions:self];
273 | [[self window] close];
274 | }
275 |
276 | - (IBAction)showAdvancedWindow:(NSButton *)sender {
277 | self.advancedWindowController = [[AdvancedWindowController alloc] initWithWindowNibName:@"AdvancedWindow" parentController:self];
278 | [[self window] beginSheet:self.advancedWindowController.window completionHandler:^(NSModalResponse returnCode) {
279 | if (returnCode == NSModalResponseOK) {
280 | self.outbounds = self.advancedWindowController.outbounds;
281 | self.cusProfiles = self.advancedWindowController.configs;
282 | self.routingRuleSets = self.advancedWindowController.routingRuleSets;
283 | self.subscriptions = self.advancedWindowController.subscriptions;
284 | self.enableRestore = self.advancedWindowController.enableRestore;
285 | self.enableEncryption = self.advancedWindowController.enableEncryption;
286 | self.encryptionKey = self.advancedWindowController.encryptionKey;
287 | }
288 | self.advancedWindowController = nil;
289 | }];
290 | }
291 |
292 |
293 | - (IBAction)showTransportSettings:(id)sender {
294 | if ([_profiles count] == 0) {
295 | return;
296 | }
297 | self.transportWindowController = [[TransportWindowController alloc] initWithWindowNibName:@"TransportWindow" parentController:self];
298 | [[self window] beginSheet:self.transportWindowController.window completionHandler:^(NSModalResponse returnCode) {
299 | if (returnCode == NSModalResponseOK) {
300 | NSArray* a = [self->_transportWindowController generateSettings];
301 | self.selectedProfile.streamSettings = a[0];
302 | self.selectedProfile.muxSettings = a[1];
303 | }
304 | self.transportWindowController = nil;
305 | }];
306 | }
307 |
308 | // https://stackoverflow.com/questions/7387341/how-to-create-and-get-return-value-from-cocoa-dialog/7387395#7387395
309 | - (void)askInputWithPrompt: (NSString*)prompt handler:(void (^ __nullable)(NSString* inputStr))handler {
310 |
311 | NSAlert *alert = [[NSAlert alloc] init];
312 | alert.messageText = prompt;
313 | [alert addButtonWithTitle:@"OK"];
314 | [alert addButtonWithTitle:@"Cancel"];
315 | NSTextField *inputField = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 400, 24)];
316 | inputField.usesSingleLineMode = true;
317 | inputField.lineBreakMode = NSLineBreakByTruncatingHead;
318 | [alert setAccessoryView:inputField];
319 | [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
320 | if (returnCode == NSAlertFirstButtonReturn) {
321 | handler([inputField stringValue]);
322 | }
323 | }];
324 | }
325 |
326 | - (void)showAlert:(NSString*)text {
327 | NSAlert* alert = [[NSAlert alloc] init];
328 | [alert setInformativeText:text];
329 | [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
330 | ;
331 | }];
332 | }
333 |
334 | - (IBAction)importFromStandardLink:(id)sender {
335 | [self askInputWithPrompt:@"Support standard vmess:// and ss:// link. Standard vmess:// link is still under discussion. Use \"Import from other links...\" to import other links, for example, vmess:// invented by v2rayN." handler:^(NSString *inputStr) {
336 | if (inputStr.length) {
337 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
338 | NSMutableDictionary* ssOutbound = [ConfigImporter ssOutboundFromSSLink:inputStr];
339 | if (ssOutbound) {
340 | [self.outbounds addObject:ssOutbound];
341 | [self presentImportResultOfVmessCount:0 otherCount:1 ruleSetCount:0];
342 | } else {
343 | [self presentImportResultOfVmessCount:0 otherCount:0 ruleSetCount:0];
344 | }
345 | });
346 | }
347 | }];
348 | }
349 |
350 | - (void)presentImportResultOfVmessCount:(NSInteger)vmessCount otherCount:(NSInteger)otherCount ruleSetCount:(NSInteger)ruleSetCount {
351 | dispatch_async(dispatch_get_main_queue(), ^{
352 | [self->_profileTable reloadData];
353 | self.popover = [[NSPopover alloc] init];
354 | self.importMessageField.stringValue = [NSString stringWithFormat:@"Imported %lu vmess and %lu other protocol outbounds, %lu routing rule sets.", vmessCount, otherCount, ruleSetCount];
355 | self.popover.contentViewController = [[NSViewController alloc] init];
356 | self.popover.contentViewController.view = self.importResultView;
357 | self.popover.behavior = NSPopoverBehaviorTransient;
358 | [self.popover showRelativeToRect:[self.importButton bounds] ofView:self.importButton preferredEdge:NSMaxYEdge];
359 | });
360 |
361 | }
362 |
363 | - (IBAction)importFromMiscLinks:(id)sender {
364 | [self askInputWithPrompt:@"V2RayX will try importing ssd://, vmess:// and http(s):// links from v2rayN and SSD(may cause failure)." handler:^(NSString *inputStr) {
365 | inputStr = [inputStr stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
366 | if ([inputStr length] != 0) {
367 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
368 | ServerProfile* p = [ConfigImporter importFromVmessOfV2RayN:inputStr];
369 | NSInteger vmessCount = 0;
370 | NSInteger otherCount = 0;
371 | if (p) {
372 | [self.profiles addObject:p];
373 | vmessCount = 1;
374 | }
375 | NSDictionary* ssdResult = [ConfigImporter importFromSubscriptionOfSSD:inputStr];
376 | for (NSDictionary* d in ssdResult[@"other"]) {
377 | [self.outbounds addObject:[d mutableDeepCopy]];
378 | otherCount += 1;
379 | }
380 | NSMutableDictionary* p2 = [ConfigImporter importFromHTTPSubscription:inputStr];
381 | if (p2) {
382 | [self.profiles addObjectsFromArray:p2[@"vmess"]];
383 | [self.outbounds addObjectsFromArray:p2[@"other"]];
384 | vmessCount += [p2[@"vmess"] count];
385 | otherCount += [p2[@"other"] count];
386 | }
387 | [self presentImportResultOfVmessCount:vmessCount otherCount:otherCount ruleSetCount:0];
388 | });
389 | }
390 | }];
391 | }
392 |
393 | - (IBAction)importFromJSONFiles:(id)sender {
394 | NSOpenPanel* openPanel = [NSOpenPanel openPanel];
395 | [openPanel setCanChooseFiles:YES];
396 | [openPanel setAllowsMultipleSelection:YES];
397 | [openPanel setAllowedFileTypes:@[@"json"]];
398 | [openPanel setDirectoryURL:[[NSFileManager defaultManager] homeDirectoryForCurrentUser]];
399 |
400 | [openPanel beginSheetModalForWindow:[self window] completionHandler:^(NSModalResponse result) {
401 | if (result == NSModalResponseOK) {
402 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
403 | NSArray* files = [openPanel URLs];
404 | NSMutableDictionary* result = [ConfigImporter importFromStandardConfigFiles:files];
405 | [self.profiles addObjectsFromArray:result[@"vmess"]];
406 | [self.outbounds addObjectsFromArray:result[@"other"]];
407 | [self.routingRuleSets addObjectsFromArray:result[@"rules"]];
408 | [self presentImportResultOfVmessCount:[result[@"vmess"] count] otherCount:[result[@"other"] count] ruleSetCount:[result[@"rules"] count]];
409 | });
410 | }
411 | }];
412 | }
413 |
414 | - (IBAction)showLog:(id)sender {
415 | [_appDelegate viewLog:sender];
416 | }
417 |
418 | @end
419 |
--------------------------------------------------------------------------------
/V2RayX/Credits.rtf:
--------------------------------------------------------------------------------
1 | {\rtf1\ansi\ansicpg1252\cocoartf1671\cocoasubrtf200
2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fswiss\fcharset0 Arial-BoldMT;\f2\fswiss\fcharset0 Helvetica-Bold;
3 | \f3\fswiss\fcharset0 Helvetica-Oblique;}
4 | {\colortbl;\red255\green255\blue255;}
5 | {\*\expandedcolortbl;;}
6 | \vieww9000\viewh8400\viewkind0
7 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0
8 |
9 | \f0\fs24 \cf0 \
10 | \pard\pardeftab720\qc\partightenfactor0
11 |
12 | \f1\b\fs28 \cf0 \expnd0\expndtw0\kerning0
13 | Stand on the shoulders of giants
14 | \f2\fs24 \kerning1\expnd0\expndtw0 \
15 | \
16 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\pardirnatural\qc\partightenfactor0
17 | \cf0 V2RayX - GUI for V2Ray on macOS
18 | \f0\b0 \
19 |
20 | \f3\i Cenmrev
21 | \f0\i0 \
22 | https://github.com/Cenmrev/V2RayX\
23 | \
24 | For information on V2Ray, visit:\
25 | https://www.v2ray.com\
26 | \
27 | Icon "copy" made by {\field{\*\fldinst{HYPERLINK "https://www.flaticon.com/authors/freepik"}}{\fldrslt Freepik}} from www.flaticon.com \
28 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\pardirnatural\partightenfactor0
29 | \cf0 \
30 | \
31 | \
32 | }
--------------------------------------------------------------------------------
/V2RayX/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.5.1
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1297
25 | LSMinimumSystemVersion
26 | $(MACOSX_DEPLOYMENT_TARGET)
27 | LSUIElement
28 |
29 | NSHumanReadableCopyright
30 | Copyright © 2019 Cenmrev. All rights reserved.
31 | NSMainNibFile
32 | MainMenu
33 | NSPrincipalClass
34 | NSApplication
35 |
36 |
37 |
--------------------------------------------------------------------------------
/V2RayX/MutableDeepCopying.h:
--------------------------------------------------------------------------------
1 | // https://gist.github.com/yfujiki/1664847
2 |
3 | #import
4 |
5 | @protocol MutableDeepCopying
6 | -(id) mutableDeepCopy;
7 | @end
8 | @interface NSDictionary (MutableDeepCopy)
9 | @end
10 | @interface NSArray (MutableDeepCopy)
11 | @end
12 |
13 | // Implementation
14 | @implementation NSDictionary (MutableDeepCopy)
15 | - (NSMutableDictionary *) mutableDeepCopy {
16 | NSMutableDictionary * returnDict = [[NSMutableDictionary alloc] initWithCapacity:self.count];
17 | NSArray * keys = [self allKeys];
18 | for(id key in keys) {
19 | id aValue = [self objectForKey:key];
20 | id theCopy = nil;
21 | if([aValue conformsToProtocol:@protocol(MutableDeepCopying)]) {
22 | theCopy = [aValue mutableDeepCopy];
23 | } else if([aValue conformsToProtocol:@protocol(NSMutableCopying)]) {
24 | theCopy = [aValue mutableCopy];
25 | } else if([aValue conformsToProtocol:@protocol(NSCopying)]){
26 | theCopy = [aValue copy];
27 | } else {
28 | theCopy = aValue;
29 | }
30 | [returnDict setValue:theCopy forKey:key];
31 | }
32 | return returnDict;
33 | }
34 | @end
35 |
36 | @implementation NSArray (MutableDeepCopy)
37 | -(NSMutableArray *)mutableDeepCopy {
38 | NSMutableArray *returnArray = [[NSMutableArray alloc] initWithCapacity:self.count];
39 | for(id aValue in self) {
40 | id theCopy = nil;
41 | if([aValue conformsToProtocol:@protocol(MutableDeepCopying)]) {
42 | theCopy = [aValue mutableDeepCopy];
43 | } else if([aValue conformsToProtocol:@protocol(NSMutableCopying)]) {
44 | theCopy = [aValue mutableCopy];
45 | } else if([aValue conformsToProtocol:@protocol(NSCopying)]){
46 | theCopy = [aValue copy];
47 | } else {
48 | theCopy = aValue;
49 | }
50 | [returnArray addObject:theCopy];
51 | }
52 | return returnArray;
53 | }
54 | @end
55 |
--------------------------------------------------------------------------------
/V2RayX/NSData+AES256Encryption.h:
--------------------------------------------------------------------------------
1 | //
2 | // NSData+AES256Encryption.h
3 | // V2RayX
4 | //
5 | //
6 |
7 |
8 | #import
9 |
10 | @interface NSData (AES256Encryption)
11 | - (NSData *)encryptedDataWithKey:(NSString*)key;
12 | - (NSData* )decryptedDataWithKey:(NSString*)key;
13 | @end
14 |
15 |
--------------------------------------------------------------------------------
/V2RayX/NSData+AES256Encryption.m:
--------------------------------------------------------------------------------
1 | //
2 | // NSData+AES256Encryption.m
3 | // V2RayX
4 | //
5 | //
6 |
7 | #import "NSData+AES256Encryption.h"
8 | #import
9 | #import
10 |
11 | @implementation NSData (AES256Encryption)
12 |
13 | - (NSData *)encryptedDataWithKey:(NSString*)key {
14 | NSUInteger dataLength = [self length];
15 | size_t bufferSize = dataLength + kCCBlockSizeAES128;
16 | void *buffer = malloc( bufferSize );
17 | //NSLog(@"%@, %@", key);
18 | size_t numBytesEncrypted = 0;
19 | CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt, kCCAlgorithmAES, kCCOptionPKCS7Padding|kCCOptionECBMode,
20 | [key cStringUsingEncoding:NSUTF8StringEncoding], kCCKeySizeAES256,
21 | NULL /* initialization vector */,
22 | [self bytes], dataLength, /* input */
23 | buffer, bufferSize, /* output */
24 | &numBytesEncrypted);
25 | if(cryptStatus == kCCSuccess) {
26 | return [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
27 | }
28 | free(buffer);
29 | return nil;
30 | }
31 |
32 | -(NSData* )decryptedDataWithKey:(NSString*)key {
33 | NSUInteger dataLength = [self length];
34 | size_t bufferSize = dataLength + kCCBlockSizeAES128;
35 | void *buffer = malloc( bufferSize );
36 | size_t numBytesDecrypted = 0;
37 | CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt, kCCAlgorithmAES, kCCOptionPKCS7Padding|kCCOptionECBMode,
38 | [key cStringUsingEncoding:NSUTF8StringEncoding], kCCKeySizeAES256,
39 | NULL /* initialization vector */,
40 | [self bytes], dataLength, /* input */
41 | buffer, bufferSize, /* output */
42 | &numBytesDecrypted);
43 | if(cryptStatus == kCCSuccess) {
44 | return [NSData dataWithBytesNoCopy:buffer length:numBytesDecrypted];
45 | }
46 | return nil;
47 | }
48 |
49 | @end
50 |
--------------------------------------------------------------------------------
/V2RayX/ServerProfile.h:
--------------------------------------------------------------------------------
1 | //
2 | // ServerProfile.h
3 | // V2RayX
4 | //
5 | // Copyright © 2016年 Cenmrev. All rights reserved.
6 | //
7 |
8 | #import
9 | #import "AppDelegate.h"
10 | #import "utilities.h"
11 |
12 | typedef enum SecurityType : NSUInteger {
13 | auto_,
14 | aes_128_gcm,
15 | chacha20_poly130,
16 | none
17 | } SecurityType;
18 |
19 | typedef enum NetWorkType : NSUInteger {
20 | tcp,
21 | kcp,
22 | ws,
23 | http,
24 | quic
25 | } NetWorkType;
26 |
27 | @interface ServerProfile : NSObject
28 | - (NSMutableDictionary*)outboundProfile;
29 | + (ServerProfile* _Nullable )readFromAnOutboundDic:(NSDictionary*)outDict;
30 | + (NSArray*)profilesFromJson:(NSDictionary*)outboundJson;
31 | -(ServerProfile*)deepCopy;
32 |
33 | @property (nonatomic) NSString* address;
34 | @property (nonatomic) NSUInteger port;
35 | @property (nonatomic) NSString* userId;
36 | @property (nonatomic) NSUInteger alterId;
37 | @property (nonatomic) NSUInteger level;
38 | @property (nonatomic) NSString* outboundTag;
39 | @property (nonatomic) SecurityType security;
40 | @property (nonatomic) NetWorkType network;
41 | @property (nonatomic) NSString* sendThrough;
42 | @property (nonatomic) NSDictionary* streamSettings; // except network type.
43 | @property (nonatomic) NSDictionary* muxSettings;
44 | @end
45 |
--------------------------------------------------------------------------------
/V2RayX/ServerProfile.m:
--------------------------------------------------------------------------------
1 | //
2 | // ServerProfile.m
3 | // V2RayX
4 | //
5 | // Copyright © 2016年 Cenmrev. All rights reserved.
6 | //
7 |
8 | #import "ServerProfile.h"
9 |
10 | @implementation ServerProfile
11 |
12 | - (ServerProfile*)init {
13 | self = [super init];
14 | if (self) {
15 | [self setAddress:@"server.cc"];
16 | [self setPort:10086];
17 | [self setUserId:@"00000000-0000-0000-0000-000000000000"];
18 | [self setAlterId:64];
19 | [self setLevel:0];
20 | [self setOutboundTag:@"test server"];
21 | [self setSecurity:auto_];
22 | [self setNetwork:tcp];
23 | [self setSendThrough:@"0.0.0.0"];
24 | [self setStreamSettings:@{
25 | @"security": @"none",
26 | @"tlsSettings": @{
27 | @"serverName": @"server.cc",
28 | @"alpn": @[@"http/1.1"],
29 | @"allowInsecure": [NSNumber numberWithBool:NO],
30 | @"allowInsecureCiphers": [NSNumber numberWithBool:NO]
31 | },
32 | @"tcpSettings": @{
33 | @"header": @{
34 | @"type": @"none"
35 | }
36 | },
37 | @"kcpSettings": @{
38 | @"mtu": @1350,
39 | @"tti": @20,
40 | @"uplinkCapacity": @5,
41 | @"downlinkCapacity": @20,
42 | @"congestion": [NSNumber numberWithBool:NO],
43 | @"readBufferSize": @1,
44 | @"writeBufferSize": @1,
45 | @"header": @{
46 | @"type": @"none"
47 | }
48 | },
49 | @"wsSettings": @{
50 | @"path": @"",
51 | @"headers": @{}
52 | },
53 | @"httpSettings": @{
54 | @"host": @[@""],
55 | @"path": @""
56 | },
57 | @"quicSettings": @{
58 | @"security": @"none",
59 | @"key": @"",
60 | @"header": @{ @"type": @"none" }
61 | },
62 | @"sockopt": @{}
63 | }];
64 | [self setMuxSettings:@{
65 | @"enabled": [NSNumber numberWithBool:NO],
66 | @"concurrency": @8
67 | }];
68 | }
69 | return self;
70 | }
71 |
72 | - (NSString*)description {
73 | return [[self outboundProfile] description];
74 | }
75 |
76 | + (NSArray*)profilesFromJson:(NSDictionary*)outboundJson {
77 | if (![outboundJson[@"protocol"] isKindOfClass:[NSString class]]
78 | || ![outboundJson[@"protocol"] isEqualToString:@"vmess"] ) {
79 | return @[];
80 | }
81 | NSMutableArray* profiles = [[NSMutableArray alloc] init];
82 | NSString* sendThrough = nilCoalescing(outboundJson[@"sendThrough"], @"0.0.0.0");
83 | if (![[outboundJson valueForKeyPath:@"settings.vnext"] isKindOfClass:[NSArray class]]) {
84 | return @[];
85 | }
86 | for (NSDictionary* vnext in [outboundJson valueForKeyPath:@"settings.vnext"]) {
87 | ServerProfile* profile = [[ServerProfile alloc] init];
88 | profile.address = nilCoalescing(vnext[@"address"], @"127.0.0.1");
89 | profile.outboundTag = nilCoalescing(outboundJson[@"tag"], @"");
90 | profile.port = [vnext[@"port"] unsignedIntegerValue];
91 | if (![vnext[@"users"] isKindOfClass:[NSArray class]] || [vnext[@"users"] count] == 0) {
92 | continue;
93 | }
94 | profile.userId = nilCoalescing(vnext[@"users"][0][@"id"], @"23ad6b10-8d1a-40f7-8ad0-e3e35cd38287");
95 | profile.alterId = [vnext[@"users"][0][@"alterId"] unsignedIntegerValue];
96 | profile.level = [vnext[@"users"][0][@"level"] unsignedIntegerValue];
97 | profile.security = searchInArray(vnext[@"users"][0][@"security"], VMESS_SECURITY_LIST);
98 | if (outboundJson[@"streamSettings"] != nil) {
99 | profile.streamSettings = outboundJson[@"streamSettings"];
100 | profile.network = searchInArray(outboundJson[@"streamSettings"][@"network"], NETWORK_LIST);
101 | }
102 | if (outboundJson[@"mux"] != nil) {
103 | profile.muxSettings = outboundJson[@"mux"];
104 | }
105 | profile.sendThrough = sendThrough;
106 | [profiles addObject:profile];
107 | }
108 | return profiles;
109 | }
110 |
111 | + (ServerProfile* _Nullable )readFromAnOutboundDic:(NSDictionary*)outDict {
112 | NSArray *allProfiles = [self profilesFromJson:outDict];
113 | if ([allProfiles count] > 0) {
114 | return allProfiles[0];
115 | } else {
116 | return NULL;
117 | }
118 | }
119 |
120 | -(ServerProfile*)deepCopy {
121 | ServerProfile* aCopy = [[ServerProfile alloc] init];
122 | aCopy.address = [NSString stringWithString:nilCoalescing(self.address, @"")];
123 | aCopy.port = self.port;
124 | aCopy.userId = [NSString stringWithString:nilCoalescing(self.userId, @"")];
125 | aCopy.alterId = self.alterId;
126 | aCopy.level = self.level;
127 | aCopy.outboundTag = [NSString stringWithString:nilCoalescing(self.outboundTag, @"")];
128 | aCopy.security = self.security;
129 | aCopy.network = self.network;
130 | aCopy.sendThrough = [NSString stringWithString:nilCoalescing(self.sendThrough, @"")];
131 | aCopy.streamSettings = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:self.streamSettings]];
132 | aCopy.muxSettings = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:self.muxSettings]];
133 | return aCopy;
134 | }
135 |
136 | - (NSMutableDictionary*)outboundProfile {
137 | NSMutableDictionary* fullStreamSettings = [NSMutableDictionary dictionaryWithDictionary:streamSettings];
138 | fullStreamSettings[@"network"] = NETWORK_LIST[network];
139 | NSDictionary* result =
140 | @{
141 | @"sendThrough": sendThrough,
142 | @"tag": nilCoalescing(outboundTag, @""),
143 | @"protocol": @"vmess",
144 | @"settings": [@{
145 | @"vnext": @[
146 | @{
147 | @"address": nilCoalescing([address stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] , @""),
148 | @"port": [NSNumber numberWithUnsignedInteger:port],
149 | @"users": @[
150 | @{
151 | @"id": userId != nil ? [userId stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]: @"",
152 | @"alterId": [NSNumber numberWithUnsignedInteger:alterId],
153 | @"security": VMESS_SECURITY_LIST[security],
154 | @"level": [NSNumber numberWithUnsignedInteger:level]
155 | }
156 | ]
157 | }
158 | ]
159 | } mutableCopy],
160 | @"streamSettings": fullStreamSettings,
161 | @"mux": muxSettings,
162 | };
163 | return [result mutableCopy];
164 | }
165 |
166 | @synthesize address;
167 | @synthesize port;
168 | @synthesize userId;
169 | @synthesize alterId;
170 | @synthesize level;
171 | @synthesize outboundTag;
172 | @synthesize security;
173 | @synthesize network;
174 | @synthesize sendThrough;
175 | @synthesize muxSettings;
176 | @synthesize streamSettings;
177 | @end
178 |
--------------------------------------------------------------------------------
/V2RayX/TransportWindowController.h:
--------------------------------------------------------------------------------
1 | //
2 | // TransportWindowController.h
3 | // V2RayX
4 | //
5 | //
6 |
7 | #import
8 | #import "ConfigWindowController.h"
9 | #import "utilities.h"
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | @interface TransportWindowController : NSWindowController
14 |
15 | - (instancetype)initWithWindowNibName:(NSNibName)windowNibName parentController:(ConfigWindowController*)parent;
16 |
17 | - (BOOL)checkInputs;
18 | - (NSArray*)generateSettings;
19 |
20 | //kcp fields
21 | @property (weak) IBOutlet NSTextField *kcpMtuField;
22 | @property (weak) IBOutlet NSTextField *kcpTtiField;
23 | @property (weak) IBOutlet NSTextField *kcpUcField;
24 | @property (weak) IBOutlet NSTextField *kcpDcField;
25 | @property (weak) IBOutlet NSTextField *kcpRbField;
26 | @property (weak) IBOutlet NSTextField *kcpWbField;
27 | @property (weak) IBOutlet NSPopUpButton *kcpCongestionButton;
28 | @property (weak) IBOutlet NSPopUpButton *kcpHeaderTypeButton;
29 | //tcp fields
30 | @property (weak) IBOutlet NSButton *tcpHeaderCusButton;
31 | @property (unsafe_unretained) IBOutlet NSTextView *tcpHeaderField;
32 |
33 |
34 | //ws fields
35 | @property (weak) IBOutlet NSTextField *wsPathField;
36 | @property (unsafe_unretained) IBOutlet NSTextView *wsHeaderField;
37 | //https fields
38 | @property (weak) IBOutlet NSTextField *httpHostsField;
39 | @property (weak) IBOutlet NSTextField *httpPathField;
40 |
41 | // quic fields
42 | @property (weak) IBOutlet NSPopUpButton *quicSecurityButton;
43 | @property (weak) IBOutlet NSTextField *quicKeyField;
44 | @property (weak) IBOutlet NSPopUpButton *quicHeaderButton;
45 |
46 |
47 | //tls fields
48 | @property (weak) IBOutlet NSButton *tlsUseButton;
49 | @property (weak) IBOutlet NSButton *tlsAiButton;
50 | @property (weak) IBOutlet NSButton *tlsAllowInsecureCiphersButton;
51 | @property (weak) IBOutlet NSTextField *tlsAlpnField;
52 | @property (weak) IBOutlet NSTextField *tlsServerNameField;
53 |
54 | //mux fields
55 | @property (weak) IBOutlet NSButton *muxEnableButton;
56 | @property (weak) IBOutlet NSTextField *muxConcurrencyField;
57 |
58 | //tcp fast open
59 | @property (weak) IBOutlet NSButton *tfoEnableButton;
60 |
61 | @end
62 |
63 | NS_ASSUME_NONNULL_END
64 |
--------------------------------------------------------------------------------
/V2RayX/TransportWindowController.m:
--------------------------------------------------------------------------------
1 | //
2 | // TransportWindowController.m
3 | // V2RayX
4 | //
5 | //
6 |
7 | #import "TransportWindowController.h"
8 |
9 | @interface TransportWindowController () {
10 | ConfigWindowController* configWindowController;
11 | }
12 |
13 | @end
14 |
15 | @implementation TransportWindowController
16 |
17 | - (instancetype)initWithWindowNibName:(NSNibName)windowNibName parentController:(ConfigWindowController*)parent {
18 | self = [super initWithWindowNibName:windowNibName];
19 | if (self) {
20 | configWindowController = parent;
21 | }
22 | return self;
23 | }
24 |
25 | - (void)windowDidLoad {
26 | [super windowDidLoad];
27 |
28 | //add UI items
29 | [_kcpHeaderTypeButton removeAllItems];
30 | [_quicHeaderButton removeAllItems];
31 | for (NSString* header in OBFU_LIST) {
32 | [_kcpHeaderTypeButton addItemWithTitle:header];
33 | [_quicHeaderButton addItemWithTitle:header];
34 | }
35 | [_quicSecurityButton removeAllItems];
36 | for (NSString* security in QUIC_SECURITY_LIST) {
37 | [_quicSecurityButton addItemWithTitle:security];
38 | }
39 |
40 | //set display
41 | NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
42 | [formatter setNumberStyle:NSNumberFormatterNoStyle];
43 | [_kcpMtuField setFormatter:formatter];
44 | [_kcpTtiField setFormatter:formatter];
45 | [_kcpUcField setFormatter:formatter];
46 | [_kcpDcField setFormatter:formatter];
47 | [_kcpRbField setFormatter:formatter];
48 | [_kcpWbField setFormatter:formatter];
49 | [_muxConcurrencyField setFormatter:formatter];
50 | // _tcpHdField setautomatcic
51 | [_tcpHeaderField setAutomaticQuoteSubstitutionEnabled:false];
52 | [_wsHeaderField setAutomaticQuoteSubstitutionEnabled:false];
53 |
54 | //read settings
55 | [self fillStream:configWindowController.selectedProfile.streamSettings andMuxSettings:configWindowController.selectedProfile.muxSettings];
56 |
57 | }
58 |
59 | - (void)fillStream:(NSDictionary*)streamSettings andMuxSettings:(NSDictionary*)muxSettings {
60 | //kcp
61 | [_kcpMtuField setIntegerValue:[streamSettings[@"kcpSettings"][@"mtu"] integerValue]];
62 | [_kcpTtiField setIntegerValue:[streamSettings[@"kcpSettings"][@"tti"] integerValue]];
63 | [_kcpUcField setIntegerValue:[streamSettings[@"kcpSettings"][@"uplinkCapacity"] integerValue]];
64 | [_kcpDcField setIntegerValue:[streamSettings[@"kcpSettings"][@"downlinkCapacity"] integerValue]];
65 | [_kcpRbField setIntegerValue:[streamSettings[@"kcpSettings"][@"readBufferSize"] integerValue]];
66 | [_kcpWbField setIntegerValue:[streamSettings[@"kcpSettings"][@"writeBufferSize"] integerValue]];
67 | [_kcpCongestionButton selectItemAtIndex:[streamSettings[@"kcpSettings"][@"congestion"] boolValue] ? 1 : 0];
68 | [_kcpHeaderTypeButton selectItemAtIndex:searchInArray(streamSettings[@"kcpSettings"][@"header"][@"type"], OBFU_LIST)];
69 |
70 | //tcp
71 | [_tcpHeaderCusButton setState:[streamSettings[@"tcpSettings"][@"header"][@"type"] isEqualToString:@"http"] ? 1 : 0];
72 | if ([_tcpHeaderCusButton state]) {
73 | [_tcpHeaderField setString:
74 | [[NSString alloc]initWithData:[NSJSONSerialization dataWithJSONObject:streamSettings[@"tcpSettings"][@"header"] options:NSJSONWritingPrettyPrinted error:nil] encoding:NSUTF8StringEncoding]];
75 | } else {
76 | [_tcpHeaderField setString:@"{\"type\": \"none\"}"];
77 | }
78 | //websocket
79 | NSString *savedWsPath = streamSettings[@"wsSettings"][@"path"];
80 | [_wsPathField setStringValue: savedWsPath != nil ? savedWsPath : @""];
81 | if (streamSettings[@"wsSettings"][@"headers"] != nil) {
82 | [_wsHeaderField setString:[[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:streamSettings[@"wsSettings"][@"headers"] options:NSJSONWritingPrettyPrinted error:nil] encoding:NSUTF8StringEncoding]];
83 | } else {
84 | [_wsHeaderField setString:@"{}"];
85 | }
86 | //http/2
87 | [_httpPathField setStringValue:nilCoalescing(streamSettings[@"httpSettings"][@"path"], @"")];
88 | NSString* hostString = @"";
89 | if ([streamSettings[@"httpSettings"] objectForKey:@"host"]) {
90 | NSArray* hostArray = streamSettings[@"httpSettings"][@"host"];
91 | if([hostArray isKindOfClass:[NSArray class]] && [hostArray count] > 0) {
92 | hostString = [hostArray componentsJoinedByString:@","];
93 | }
94 | }
95 | [_httpHostsField setStringValue:hostString];
96 | //quic
97 | [_quicKeyField setStringValue:nilCoalescing(streamSettings[@"quicSettings"][@"key"], @"")];
98 | [_quicSecurityButton selectItemAtIndex:searchInArray(streamSettings[@"quicSettings"][@"security"], QUIC_SECURITY_LIST)];
99 | [_quicHeaderButton selectItemAtIndex:searchInArray(streamSettings[@"quicSettings"][@"header"][@"type"], OBFU_LIST)];
100 |
101 | //tls
102 | [_tlsUseButton setState:[[streamSettings objectForKey:@"security"] boolValue]];
103 | NSDictionary* tlsSettings = [streamSettings objectForKey:@"tlsSettings"];
104 | [_tlsAiButton setState:[tlsSettings[@"allowInsecure"] boolValue]];
105 | [_tlsAllowInsecureCiphersButton setState:[tlsSettings[@"allowInsecureCiphers"] boolValue]];
106 | NSArray* alpnArray = streamSettings[@"tlsSettings"][@"alpn"];
107 | NSString* alpnString = [alpnArray componentsJoinedByString:@","];
108 | [_tlsAlpnField setStringValue:nilCoalescing(alpnString, @"http/1.1")];
109 | [_tlsServerNameField setStringValue:streamSettings[@"tlsSettings"][@"serverName"]];
110 |
111 | [self useTLS:nil];
112 | // mux
113 | [_muxEnableButton setState:[nilCoalescing(muxSettings[@"enabled"], @NO) boolValue]];
114 | [_muxConcurrencyField setIntegerValue:[nilCoalescing(muxSettings[@"concurrency"], @8) integerValue]];
115 | // tcp fast open
116 | NSDictionary* tfoSettings = [streamSettings objectForKey:@"sockopt"];
117 | [_tfoEnableButton setState:[tfoSettings[@"tcpFastOpen"] boolValue]];
118 | }
119 |
120 | - (IBAction)useTLS:(id)sender {
121 | [_tlsAiButton setEnabled:[_tlsUseButton state]];
122 | [_tlsAllowInsecureCiphersButton setEnabled:[_tlsUseButton state]];
123 | }
124 |
125 | - (IBAction)tReset:(id)sender {
126 | ServerProfile *p = [[ServerProfile alloc] init];
127 | [self fillStream:p.streamSettings andMuxSettings:p.muxSettings];
128 | }
129 |
130 | - (IBAction)showTcpHeaderExample:(id)sender {
131 | runCommandLine(@"/usr/bin/open", @[[[NSBundle mainBundle] pathForResource:@"tcp_http_header_example" ofType:@"txt"], @"-a", @"/Applications/TextEdit.app"]);
132 | }
133 |
134 | - (IBAction)tCancel:(id)sender {
135 | [self.window.sheetParent endSheet:self.window returnCode:NSModalResponseCancel];
136 | }
137 |
138 | - (IBAction)ok:(id)sender {
139 | NSLog(@"%@", [_httpPathField stringValue]);
140 | if ([self checkInputs]) {
141 | [self.window.sheetParent endSheet:self.window returnCode:NSModalResponseOK];
142 | }
143 | }
144 |
145 | - (IBAction)transportHelp:(id)sender {
146 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://v2ray.com/chapter_02/05_transport.html"]];
147 | }
148 |
149 | - (BOOL)checkInputs {
150 | NSError* httpHeaderParseError;
151 | if ([[_tcpHeaderField string] length] == 0) {
152 | [_tcpHeaderField setString:TCP_NONE_HEADER_OBJECT];
153 | }
154 | [NSJSONSerialization JSONObjectWithData:[[_tcpHeaderField string] dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&httpHeaderParseError];
155 | if (httpHeaderParseError) {
156 | NSAlert* parseAlert = [[NSAlert alloc] init];
157 | [parseAlert setMessageText:@"Error in parsing customized tcp http header!"];
158 | [parseAlert beginSheetModalForWindow:[self window] completionHandler:^(NSModalResponse returnCode) { }];
159 | return NO;
160 | }
161 |
162 | NSError* wsHeaderParseError;
163 | if ([[_wsHeaderField string] length] == 0) {
164 | [_wsHeaderField setString:@"{}"];
165 | }
166 | [NSJSONSerialization JSONObjectWithData:[[_wsHeaderField string] dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&wsHeaderParseError];
167 | if(wsHeaderParseError) {
168 | NSAlert* parseAlert = [[NSAlert alloc] init];
169 | [parseAlert setMessageText:@"Error in parsing customized WebSocket headers!"];
170 | [parseAlert beginSheetModalForWindow:[self window] completionHandler:^(NSModalResponse returnCode) { }];
171 | return NO;
172 | }
173 | return YES;
174 | }
175 |
176 | - (NSArray*)generateSettings {
177 | NSString* tcpHttpHeaderString = TCP_NONE_HEADER_OBJECT;
178 | if ([_tcpHeaderCusButton state]) {
179 | tcpHttpHeaderString = [_tcpHeaderField string];
180 | }
181 | NSDictionary* tcpHttpHeader = [NSJSONSerialization JSONObjectWithData:[tcpHttpHeaderString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
182 |
183 | NSDictionary* wsHeader = [NSJSONSerialization JSONObjectWithData:[[_wsHeaderField string] dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
184 |
185 | NSArray* httpHosts;
186 | if ([[[_httpHostsField stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] == 0) {
187 | httpHosts = @[];
188 | } else {
189 | NSString* hostsString = [[_httpHostsField stringValue] stringByReplacingOccurrencesOfString:@" " withString:@""];
190 | httpHosts = [hostsString componentsSeparatedByString:@","];
191 | }
192 | NSDictionary *httpSettings;
193 | if ([httpHosts count] > 0) {
194 | httpSettings = @{ @"host": httpHosts,
195 | @"path": [self->_httpPathField stringValue]
196 | };
197 | } else {
198 | httpSettings = @{ @"path": [self->_httpPathField stringValue] };
199 | }
200 | NSDictionary *streamSettingsImmutable =
201 | @{@"kcpSettings":
202 | @{@"mtu":[NSNumber numberWithInteger:[self->_kcpMtuField integerValue]],
203 | @"tti":[NSNumber numberWithInteger:[self->_kcpTtiField integerValue]],
204 | @"uplinkCapacity":[NSNumber numberWithInteger:[self->_kcpUcField integerValue]],
205 | @"downlinkCapacity":[NSNumber numberWithInteger:[self->_kcpDcField integerValue]],
206 | @"readBufferSize":[NSNumber numberWithInteger:[self->_kcpRbField integerValue]],
207 | @"writeBufferSize":[NSNumber numberWithInteger:[self->_kcpWbField integerValue]],
208 | @"congestion":[NSNumber numberWithBool:[self->_kcpCongestionButton indexOfSelectedItem] != 0],
209 | @"header":@{@"type":[[self->_kcpHeaderTypeButton selectedItem] title]}
210 | },
211 | @"tcpSettings":@{@"header": tcpHttpHeader},
212 | @"wsSettings": @{
213 | @"path": [[_wsPathField stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]],
214 | @"headers": nilCoalescing(wsHeader, @{})
215 | },
216 | @"quicSettings": @{
217 | @"security": [[self->_quicSecurityButton selectedItem] title],
218 | @"key": [_quicKeyField stringValue],
219 | @"header": @{
220 | @"type": [[self->_quicHeaderButton selectedItem] title]
221 | }
222 | },
223 | @"security": [self->_tlsUseButton state] ? @"tls" : @"none",
224 | @"tlsSettings": @{
225 | @"serverName": [_tlsServerNameField stringValue],
226 | @"allowInsecure": [NSNumber numberWithBool:[self->_tlsAiButton state]==1],
227 | @"allowInsecureCiphers": [NSNumber numberWithBool:[self->_tlsAllowInsecureCiphersButton state]==1],
228 | @"alpn": [[[_tlsAlpnField stringValue] stringByReplacingOccurrencesOfString:@" " withString:@""] componentsSeparatedByString:@","]
229 | },
230 | @"httpSettings": httpSettings
231 | };
232 | NSMutableDictionary *streamSettings = [streamSettingsImmutable mutableCopy];
233 | if ([self->_tfoEnableButton state]) {
234 | streamSettings[@"sockopt"] = @{
235 | @"tcpFastOpen": @(YES)
236 | };
237 | }
238 | NSDictionary* muxSettings = @{
239 | @"enabled":[NSNumber numberWithBool:[self->_muxEnableButton state]==1],
240 | @"concurrency":[NSNumber numberWithInteger:[self->_muxConcurrencyField integerValue]]
241 | };
242 | return @[streamSettings, muxSettings];
243 | }
244 |
245 |
246 | @end
247 |
--------------------------------------------------------------------------------
/V2RayX/config-sample.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | inboundDetour
6 |
7 |
8 | allocate
9 |
10 | concurrency
11 | 3
12 | refresh
13 | 5
14 | strategy
15 | always
16 |
17 | domainOverride
18 |
19 | http
20 | tls
21 |
22 | listen
23 | 127.0.0.1
24 | port
25 | 8001
26 | protocol
27 | http
28 | settings
29 |
30 | timeout
31 | 0
32 |
33 | streamSettings
34 |
35 | tag
36 | httpDetour
37 |
38 |
39 | dns
40 |
41 | servers
42 |
43 | 8.8.8.8
44 | 8.8.4.4
45 | localhost
46 |
47 |
48 | inbound
49 |
50 | listen
51 | 127.0.0.1
52 | port
53 | 1080
54 | protocol
55 | socks
56 | settings
57 |
58 | auth
59 | noauth
60 | ip
61 | 127.0.0.1
62 | udp
63 |
64 |
65 |
66 | log
67 |
68 | error
69 |
70 | access
71 |
72 | loglevel
73 | warning
74 |
75 | outbound
76 |
77 | mux
78 |
79 | enabled
80 |
81 |
82 | protocol
83 | vmess
84 | settings
85 |
86 | vnext
87 |
88 |
89 | address
90 | 192.168.1.9
91 | port
92 | 10086
93 | users
94 |
95 |
96 | alterId
97 | 64
98 | id
99 | 23ad6b10-8d1a-40f7-8ad0-e3e35cd38297
100 | security
101 | auto
102 |
103 |
104 |
105 |
106 |
107 | streamSettings
108 |
109 | network
110 | tcp
111 | security
112 | none
113 | tlsSettings
114 |
115 | allowInsecure
116 |
117 | serverName
118 | v2ray.cool
119 |
120 | wsSettings
121 |
122 | path
123 | /v2ray.cool/
124 |
125 |
126 |
127 | outboundDetour
128 |
129 |
130 | protocol
131 | freedom
132 | settings
133 |
134 | tag
135 | direct
136 |
137 |
138 | policy
139 |
140 | levels
141 |
142 | 0
143 |
144 | uplinkOnly
145 | 0
146 |
147 |
148 |
149 | routing
150 |
151 | settings
152 |
153 | domainStrategy
154 | IPIfNonMatch
155 | rules
156 |
157 |
158 | type
159 | field
160 | outboundTag
161 | direct
162 | domain
163 |
164 | localhost
165 | domain:me.com
166 | domain:lookup-api.apple.com
167 | domain:icloud-content.com
168 | domain:icloud.com
169 | domain:cdn-apple.com
170 | domain:apple-cloudkit.com
171 | domain:apple.com
172 | domain:apple.co
173 | domain:aaplimg.com
174 | domain:guzzoni.apple.com
175 |
176 |
177 |
178 | type
179 | field
180 | outboundTag
181 | direct
182 | ip
183 |
184 | geoip:private
185 |
186 |
187 |
188 |
189 | strategy
190 | rules
191 |
192 |
193 |
194 |
--------------------------------------------------------------------------------
/V2RayX/config-sample_new.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | dns
6 |
7 | servers
8 |
9 | localhost
10 |
11 |
12 | inbounds
13 |
14 |
15 | listen
16 | 127.0.0.1
17 | port
18 | 1081
19 | protocol
20 | socks
21 | settings
22 |
23 | auth
24 | noauth
25 | ip
26 | 127.0.0.1
27 | udp
28 |
29 |
30 | tag
31 | socksinbound
32 |
33 |
34 | listen
35 | 127.0.0.1
36 | port
37 | 8001
38 | protocol
39 | http
40 | settings
41 |
42 | timeout
43 | 0
44 |
45 | tag
46 | httpinbound
47 |
48 |
49 | log
50 |
51 | outbounds
52 |
53 | routing
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/V2RayX/dlcore.sh:
--------------------------------------------------------------------------------
1 | VERSION="4.18.0"
2 | RED='\033[0;31m'
3 | GREEN='\033[0;32m'
4 | BOLD='\033[1m'
5 | NORMAL='\033[0m'
6 |
7 | cd "$SRCROOT"
8 | output="v0"
9 | if [[ -f ./v2ray-core-bin/v2ray ]]; then
10 | output=$(./v2ray-core-bin/v2ray --version)
11 | fi
12 | existingVersion=${output:6:${#VERSION}}
13 | if [ "$VERSION" != "$existingVersion" ]; then
14 | getCore=0
15 | mkdir -p v2ray-core-bin
16 | cd v2ray-core-bin
17 | curl -s -L -o v2ray-macos.zip https://github.com/v2ray/v2ray-core/releases/download/v${VERSION}/v2ray-macos.zip
18 | if [[ $? == 0 ]]; then
19 | unzip -o v2ray-macos.zip
20 | getCore=1
21 | else
22 | unzip -o ~/Downloads/v2ray-macos.zip
23 | if [[ $? != 0 ]]; then
24 | getCore=0
25 | else
26 | chmod +x v2ray-${VERSION}-macos/v2ray
27 | output=$(v2ray-${VERSION}-macos/v2ray --version)
28 | existingVersion=${output:6:${#VERSION}}
29 | if [ "$VERSION" != "$existingVersion" ]; then
30 | echo "${RED}v2ray-macos.zip in the Downloads folder does not contain version ${VERSION}."
31 | echo "下载文件夹里的v2ray-macos.zip不是${VERSION}版本。${NORMAL}"
32 | getCore=0
33 | else
34 | getCore=1
35 | fi
36 | fi
37 | fi
38 | if [[ $getCore == 0 ]]; then
39 | echo "${RED}download failed!"
40 | echo "Use whatever method you can think of, get v2ray-macos.zip of version ${VERSION} from v2ray.com, and put it in the folder 'Downloads' and try this script again."
41 | echo "用你能想到任何办法,从 v2ray.com 下载好${VERSION}版本的 v2ray-macos.zip,放在“下载”文件夹里面,然后再次运行这个脚本。${NORMAL}"
42 | exit 1
43 | fi
44 | chmod +x ./v2ray
45 | chmod +x ./v2ctl
46 | rm -r v2ray-*
47 | else
48 | exit 0
49 | fi
50 |
51 |
--------------------------------------------------------------------------------
/V2RayX/install_helper.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # install_helper.sh
4 | # V2RayX
5 | #
6 | # Copyright © 2016年 Cenmrev. All rights reserved.
7 |
8 | cd `dirname "${BASH_SOURCE[0]}"`
9 | sudo mkdir -p "/Library/Application Support/V2RayX/"
10 | sudo cp v2rayx_sysconf "/Library/Application Support/V2RayX/"
11 | sudo chown root:admin "/Library/Application Support/V2RayX/v2rayx_sysconf"
12 | sudo chmod +s "/Library/Application Support/V2RayX/v2rayx_sysconf"
13 |
14 | echo done
15 |
--------------------------------------------------------------------------------
/V2RayX/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // V2RayX
4 | //
5 | // Copyright © 2016年 Cenmrev. All rights reserved.
6 | //
7 |
8 | #import
9 |
10 | int main(int argc, const char * argv[]) {
11 | return NSApplicationMain(argc, argv);
12 | }
13 |
--------------------------------------------------------------------------------
/V2RayX/simple.pac:
--------------------------------------------------------------------------------
1 | var V2Ray = "SOCKS5 127.0.0.1:1081; SOCKS 127.0.0.1:1081; DIRECT;";
2 |
3 | var domains = [
4 | "vpngate.net",
5 | "greatfire.org",
6 | "tox.im",
7 | "proxifier.com",
8 | "dnscrypt.org",
9 | "atgfw.org",
10 | "chinagfw.org",
11 | "whatismyip.com",
12 | "goagentplus.com",
13 | "shadowsocks.org",
14 | "falcop.com",
15 | "getlantern.org",
16 | "furbo.org",
17 | "goagentx.com",
18 | "github.com",
19 | "sourceforge.net",
20 | "torproject.org",
21 | "hideme.io",
22 | "mozilla.org",
23 | "shadowx.work",
24 | "v2ray.com",
25 | "astrill.com",
26 |
27 | //Design
28 | "deviantart.com",
29 |
30 | "disqus.com",
31 | "disquscdn.com",
32 | "tumblr.com",
33 | "flickr.com",
34 | "imgur.com",
35 |
36 | "evozi.com",
37 | "live.com",
38 | "1drv.com",
39 | "evernote.com",
40 |
41 | //blog
42 | "wp.com",
43 | "yam.com",
44 | "ashchan.com",
45 | "zuckclub.com",
46 | "blogspot.com",
47 | "blogspot.jp",
48 | "zalex2014.tk",
49 | "coreygilmore.com",
50 | "wordpress.com",
51 | "blogimg.jp",
52 | "leaskh.com",
53 | "blogger.com",
54 | "hexo.io",
55 | "medium.com",
56 |
57 | //GeneralNews
58 | "rfi.my",
59 | "rfi.fr",
60 | "washingtonpost.com",
61 | "tmagazine.com",
62 | "nytimes.com",
63 | "nytimg.com",
64 | "imrworldwide.com",
65 | "rankingsandreviews.com",
66 | "usnews.com",
67 | "bbc.co.uk",
68 | "bbci.co.uk",
69 | "bbc.com",
70 | "on-match.com",
71 | "solidot.org",
72 |
73 | //TechNews
74 | "engadget.com",
75 |
76 | //Network
77 | "amazonaws.com",
78 | "fastly.net",
79 | "akamaihd.net",
80 | "blogsmithmedia.com",
81 | "bit.ly",
82 | "d.pr",
83 | "ow.ly",
84 | "ift.tt",
85 |
86 | "nateparrott.com",
87 |
88 | "stacksocial.com",
89 | "feedly.com",
90 | "acgtea.com",
91 |
92 | //academy and develop
93 | "ieee.org",
94 | "mathoverflow.net",
95 | "tex.stackexchange.com",
96 | "academia.edu",
97 | "geogebra.org",
98 | "golang.org",
99 | "netspeak.org",
100 | "endreslab.com",
101 |
102 | //Universities
103 | "illinois.edu",
104 | "berkeley.edu",
105 | "wisc.edu",
106 | "cmu.edu",
107 | "rochester.edu",
108 | "purdue.edu",
109 | "technolutions.net",
110 |
111 | "netflix.com",
112 |
113 | //Online Dictionary
114 | "ldoceonline.com",
115 | "freedicts.com",
116 |
117 | //Softwares
118 | "formacx.com",
119 | "trionworlds.com",
120 | "line.me",
121 |
122 | //otaku
123 | "e-hentai.org",
124 | "nhentai.net",
125 | "nicovideo.jp",
126 | "nimg.jp",
127 | "pixnet.net",
128 | "fc2.com",
129 | "nyaatorrents.org",
130 | "nyaa.se",
131 | "share.dmhy.org",
132 | "loli.us",
133 |
134 | //facebook
135 | "facebook.net",
136 | "instagram.com",
137 | "facebook.com",
138 | "fb.me",
139 | "cdninstagram.com",
140 | "fbcdn.net",
141 |
142 | "booth.pm",
143 |
144 | "telegram.org",
145 | "telegram.me",
146 |
147 | "wikipedia.org",
148 | "pixiv.net",
149 | "twitch.tv",
150 |
151 | //Twitter
152 | "twitter.com",
153 | "t.co",
154 | "twimg.com",
155 |
156 | //Google
157 | "gmail.com",
158 | "googlemail.com",
159 | "mailchimp.com",
160 | "mail-archive.com",
161 | "google.com",
162 | "goo.gl",
163 | "google.com.hk",
164 | "google.com.tw",
165 | "google.co.jp",
166 | "googlecode.com",
167 | "googleapis.com",
168 | "ggpht.com",
169 | "youtube.com",
170 | "youtu.be",
171 | "ytimg.com",
172 | "youtube-nocookie.com",
173 | "googlevideo.com",
174 | "sketchup.com",
175 | "gstatic.com",
176 | "google-analytics.com",
177 | "googleusercontent.com",
178 | "chrome.com",
179 | "g.co",
180 | "googledrive.com",
181 | "googletagmanager.com",
182 | "googleadservices.com",
183 | "abc.xyz",
184 |
185 | //Dropbox
186 | "dropbox.com",
187 | "dropboxusercontent.com",
188 | "dropboxwiki.com",
189 |
190 | //iTuens
191 | "mzstatic.com",
192 | "itunes.apple.com",
193 |
194 | //cdn
195 | "cloudfront.net",
196 | "colwiz.com"
197 | ];
198 |
199 | function FindProxyForURL(url, host) {
200 | for (var i = domains.length - 1; i >= 0; i--) {
201 | if (dnsDomainIs(host, domains[i])) {
202 | return V2Ray;
203 | }
204 | }
205 | return "DIRECT";
206 | }
207 |
--------------------------------------------------------------------------------
/V2RayX/tcp_http_header_example.txt:
--------------------------------------------------------------------------------
1 | {
2 | "type": "http",
3 | "request": {
4 | "version": "1.1",
5 | "method": "GET",
6 | "path": ["/"],
7 | "headers": {
8 | "Host": ["www.baidu.com", "www.bing.com"],
9 | "User-Agent": [
10 | "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
11 | "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46"
12 | ],
13 | "Accept-Encoding": ["gzip, deflate"],
14 | "Connection": ["keep-alive"],
15 | "Pragma": "no-cache"
16 | }
17 | },
18 | "response": {
19 | "version": "1.1",
20 | "status": "200",
21 | "reason": "OK",
22 | "headers": {
23 | "Content-Type": ["application/octet-stream", "video/mpeg"],
24 | "Transfer-Encoding": ["chunked"],
25 | "Connection": ["keep-alive"],
26 | "Pragma": "no-cache"
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/V2RayX/utilities.h:
--------------------------------------------------------------------------------
1 | //
2 | // utilities.h
3 | // V2RayX
4 | //
5 | //
6 |
7 | #ifndef utilities_h
8 | #define utilities_h
9 |
10 | #import
11 | #import "MutableDeepCopying.h"
12 |
13 | #define DOMAIN_STRATEGY_LIST (@[@"AsIs", @"IPIfNonMatch", @"IPOnDemand"])
14 | #define ROUTING_NETWORK_LIST (@[@"tcp", @"udp", @"tcp,udp"])
15 |
16 | #define OBFU_LIST (@[@"none", @"srtp", @"utp", @"wechat-video", @"dtls", @"wireguard"])
17 | #define VMESS_SECURITY_LIST (@[@"auto", @"aes-128-gcm", @"chacha20-poly1305", @"none"])
18 | #define NETWORK_LIST (@[@"tcp", @"kcp", @"ws", @"http", @"quic"])
19 | #define QUIC_SECURITY_LIST (@[@"none", @"aes-128-gcm", @"chacha20-poly1305"])
20 | #define nilCoalescing(a,b) ( (a != nil) ? (a) : (b) ) // equivalent to ?? operator in Swift
21 |
22 | #define TCP_NONE_HEADER_OBJECT (@"{\"type\": \"none\"}")
23 |
24 | #define ROUTING_DIRECT (@{@"name": @"all_to_direct",@"domainStrategy": @"AsIs",@"rules": @[@{@"type": @"field",@"port": @"0-65535",@"outboundTag": @"direct"}]})
25 | #define ROUTING_GLOBAL (@{@"name": @"all_to_main",@"domainStrategy": @"AsIs",@"rules": @[@{@"type": @"field",@"port": @"0-65535",@"outboundTag": @"main"}]})
26 | #define ROUTING_BYPASSCN_PRIVATE_APPLE (@{@"name": @"bypasscn_private_apple",@"domainStrategy":@"IPIfNonMatch",@"rules":@[@{@"domain":@[@"localhost",@"domain:me.com",@"domain:lookup-api.apple.com",@"domain:icloud-content.com",@"domain:icloud.com",@"domain:cdn-apple.com",@"domain:apple-cloudkit.com",@"domain:apple.com",@"domain:apple.co",@"domain:aaplimg.com",@"domain:guzzoni.apple.com",@"geosite:cn"],@"type":@"field",@"outboundTag":@"direct"},@{@"type":@"field",@"outboundTag":@"direct",@"ip":@[@"geoip:private",@"geoip:cn"]},@{@"type":@"field",@"outboundTag":@"main",@"port":@"0-65535"}]})
27 |
28 | #define OUTBOUND_DIRECT (@{@"protocol":@"freedom",@"tag":@"direct",@"settings":@{}})
29 | #define OUTBOUND_DECLINE (@{@"protocol":@"blackhole",@"tag":@"decline",@"settings": @{}})
30 |
31 | #define RESERVED_TAGS (@[@"main", @"direct", @"decline", @"balance"])
32 |
33 | #define SUPPORTED_SS_SECURITY (@[@"aes-256-cfb",@"aes-128-cfb",@"chacha20",@"chacha20-ietf",@"aes-256-gcm",@"aes-128-gcm",@"chacha20-poly1305", @"chacha20-ietf-poly1305"])
34 |
35 | #define EMPTY_DICT ([[NSMutableDictionary alloc] init])
36 | #define EMPTY_IMPORT_RESULT ([@{@"vmess": @[], @"other": @[], @"rules":@[]} mutableDeepCopy])
37 | #define SNIFFING_PROTOCOL (@[@"http", @"tls", @"bittorrent"])
38 |
39 |
40 | #define NOT_SUPPORTED_ROUTING (@[@"source", @"user"])
41 | #define SUPPORTED_ROUTING (@[@"domain", @"ip", @"network", @"port", @"inboundTag", @"protocol"])
42 |
43 | NSUInteger searchInArray(NSString* str, NSArray* array);
44 |
45 |
46 | #endif /* utilities_h */
47 |
--------------------------------------------------------------------------------
/V2RayX/utilities.m:
--------------------------------------------------------------------------------
1 | //
2 | // utilities.m
3 | // V2RayX
4 | //
5 | //
6 |
7 | #import "utilities.h"
8 |
9 | NSUInteger searchInArray(NSString* str, NSArray* array) {
10 | if ([str isKindOfClass:[NSString class]]) {
11 | NSUInteger index = 0;
12 | for (NSString* s in array) {
13 | if ([s isKindOfClass:[NSString class]] && [s isEqualToString:str]) {
14 | return index;
15 | }
16 | index += 1;
17 | }
18 | }
19 | return 0;
20 | }
21 |
--------------------------------------------------------------------------------
/compilefromsource.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # compilefromsource.sh
4 | # V2RayX
5 | #
6 | # Created by Cenmrev on 10/15/16.
7 | # Copyright © 2016 Cenmrev. All rights reserved.
8 |
9 | # http://apple.stackexchange.com/questions/50844/how-to-move-files-to-trash-from-command-line
10 | function moveToTrash () {
11 | local path
12 | for path in "$@"; do
13 | # ignore any arguments
14 | if [[ "$path" = -* ]]; then :
15 | else
16 | # remove trailing slash
17 | local mindtrailingslash=${path%/}
18 | # remove preceding directory path
19 | local dst=${mindtrailingslash##*/}
20 | # append the time if necessary
21 | while [ -e ~/.Trash/"$dst" ]; do
22 | dst="`expr "$dst" : '\(.*\)\.[^.]*'` `date +%H-%M-%S`.`expr "$dst" : '.*\.\([^.]*\)'`"
23 | done
24 | mv "$path" ~/.Trash/"$dst"
25 | fi
26 | done
27 | }
28 |
29 | RED='\033[0;31m'
30 | GREEN='\033[0;32m'
31 | BOLD='\033[1m'
32 | NORMAL='\033[0m'
33 | datetime=$(date "+%Y-%m-%dTIME%H%M%S")
34 | if [[ ! -f /Applications/Xcode.app/Contents/MacOS/Xcode ]]; then
35 | echo "${RED}Xcode is needed to build V2RayX, Please install Xcode from App Store!${NORMAL}"
36 | echo "${RED}编译 V2RayX 需要 Xcode.app,请从 App Store 里安装 Xcode.${NORMAL}"
37 | else
38 | echo "${BOLD}-- Downloading source code --${NORMAL}"
39 | echo "${BOLD}-- 正在下载源码 --${NORMAL}"
40 | git clone --recursive https://github.com/Cenmrev/V2RayX.git "V2RayX${datetime}"
41 | cd "V2RayX${datetime}"
42 | echo "${BOLD}-- Start building V2RayX --${NORMAL}"
43 | echo "${BOLD}-- 开始编译 V2RayX --${NORMAL}"
44 | xcodebuild -project V2RayX.xcodeproj -target V2RayX -configuration Release
45 | if [[ $? == 0 ]]; then
46 | echo "${GREEN}-- Build succeeded --${NORMAL}"
47 | echo "${GREEN}-- 编译成功 --${NORMAL}"
48 | echo "${BOLD}V2RayX.app: $(pwd)/build/Release/V2RayX.app${NORMAL}"
49 | else
50 | echo "${RED}-- Build failed --${NORMAL}"
51 | echo "${RED}-- 编译失败 --${NORMAL}"
52 | fi
53 | fi
54 |
55 |
56 |
--------------------------------------------------------------------------------
/jsonplist/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // jsonplist
4 | //
5 | // Copyright © 2017 Project V2Ray. All rights reserved.
6 | //
7 |
8 | #import
9 |
10 | int main(int argc, const char * argv[]) {
11 | @autoreleasepool {
12 | if (argc < 2) {
13 | printf("Please provide at least one input file!\n");
14 | return 1;
15 | }
16 | NSString* imputFile = [NSString stringWithFormat:@"%s", argv[1]];
17 | NSInteger length = [imputFile length];
18 | if ([[[imputFile substringFromIndex:length - 4] lowercaseString] isEqualToString:@"json"]) {
19 | NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:
20 | [NSData dataWithContentsOfFile:imputFile] options:NSJSONReadingMutableLeaves error:nil];
21 | NSString* targetFile = [[imputFile substringToIndex:length - 4] stringByAppendingString:@"plist"];
22 | [dict writeToFile:targetFile atomically:NO];
23 | printf("%s\n", [targetFile cStringUsingEncoding:NSUTF8StringEncoding]);
24 | return 0;
25 | } else if ([[[imputFile substringFromIndex:length - 5] lowercaseString] isEqualToString:@"plist"]) {
26 | NSDictionary* dict = [[NSDictionary alloc] initWithContentsOfFile:imputFile];
27 | NSData* jsonData = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
28 | NSString* targetFile = [[imputFile substringToIndex:length - 5] stringByAppendingString:@".plist"];
29 | [jsonData writeToFile:targetFile atomically:NO];
30 | printf("%s\n", [targetFile cStringUsingEncoding:NSUTF8StringEncoding]);
31 | return 0;
32 | } else {
33 | printf("Only json and plist are supported!\n");
34 | return 1;
35 | }
36 |
37 | }
38 | return 0;
39 | }
40 |
--------------------------------------------------------------------------------
/logic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/logic.png
--------------------------------------------------------------------------------
/prepare_zip.sh:
--------------------------------------------------------------------------------
1 | isbeta=$(git describe --abbrev=0 --tags | grep beta)
2 | if [[ "$isbeta" != "" ]]
3 | then
4 | xcodebuild -project V2RayX.xcodeproj -target V2RayX -configuration Debug -s
5 | cd build/Debug/
6 | else
7 | cd build/Release/
8 | fi
9 | zip -r V2RayX.app.zip V2RayX.app
10 | cd ../..
--------------------------------------------------------------------------------
/v2rayx_sysconf/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // v2rayx_sysconf
4 | //
5 | // Copyright © 2016年 Cenmrev. All rights reserved.
6 | //
7 |
8 | #import
9 | #import
10 | #import "sysconf_version.h"
11 |
12 | #define INFO "v2rayx_sysconf\n the helper tool for V2RayX, modified from clowwindy's shadowsocks_sysconf.\nusage: v2rayx_sysconf [options]\noff\t turn off proxy\nauto\t auto proxy change\nglobal port \t global proxy at the specified port number\n"
13 |
14 | int main(int argc, const char * argv[])
15 | {
16 | if (argc < 2 || argc >4) {
17 | printf(INFO);
18 | return 1;
19 | }
20 | @autoreleasepool {
21 | NSString *mode = [NSString stringWithUTF8String:argv[1]];
22 |
23 | NSSet *support_args = [NSSet setWithObjects:@"off", @"auto", @"global", @"save", @"restore", @"-v", nil];
24 | if (![support_args containsObject:mode]) {
25 | printf(INFO);
26 | return 1;
27 | }
28 |
29 | if ([mode isEqualToString:@"-v"]) {
30 | printf("%s", [VERSION UTF8String]);
31 | return 0;
32 | }
33 |
34 | static AuthorizationRef authRef;
35 | static AuthorizationFlags authFlags;
36 | authFlags = kAuthorizationFlagDefaults
37 | | kAuthorizationFlagExtendRights
38 | | kAuthorizationFlagInteractionAllowed
39 | | kAuthorizationFlagPreAuthorize;
40 | OSStatus authErr = AuthorizationCreate(nil, kAuthorizationEmptyEnvironment, authFlags, &authRef);
41 | if (authErr != noErr) {
42 | authRef = nil;
43 | } else {
44 | if (authRef == NULL) {
45 | NSLog(@"No authorization has been granted to modify network configuration");
46 | return 1;
47 | }
48 |
49 | SCPreferencesRef prefRef = SCPreferencesCreateWithAuthorization(nil, CFSTR("V2RayX"), nil, authRef);
50 |
51 | NSDictionary *sets = (__bridge NSDictionary *)SCPreferencesGetValue(prefRef, kSCPrefNetworkServices);
52 |
53 | NSDictionary* originalSets;
54 | if ([mode isEqualToString:@"save"]) {
55 | [sets writeToURL:[NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/Library/Application Support/V2RayX/system_proxy_backup.plist",NSHomeDirectory()]] atomically:NO];
56 | return 0;
57 | }
58 |
59 | // 遍历系统中的网络设备列表,设置 AirPort 和 Ethernet 的代理
60 | if([mode isEqualToString:@"restore"]) {
61 | originalSets = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/Library/Application Support/V2RayX/system_proxy_backup.plist",NSHomeDirectory()]]];
62 | }
63 | for (NSString *key in [sets allKeys]) {
64 | NSMutableDictionary *dict = [sets objectForKey:key];
65 | NSString *hardware = [dict valueForKeyPath:@"Interface.Hardware"];
66 | // NSLog(@"%@", hardware);
67 | if ([hardware isEqualToString:@"AirPort"] || [hardware isEqualToString:@"Wi-Fi"] || [hardware isEqualToString:@"Ethernet"]) {
68 |
69 | NSMutableDictionary *proxies = [sets[key][@"Proxies"] mutableCopy];
70 | [proxies setObject:[NSNumber numberWithInt:0] forKey:(NSString *)kCFNetworkProxiesHTTPEnable];
71 | [proxies setObject:[NSNumber numberWithInt:0] forKey:(NSString *)kCFNetworkProxiesHTTPSEnable];
72 | [proxies setObject:[NSNumber numberWithInt:0] forKey:(NSString *)kCFNetworkProxiesProxyAutoConfigEnable];
73 | [proxies setObject:[NSNumber numberWithInt:0] forKey:(NSString *)kCFNetworkProxiesSOCKSEnable];
74 |
75 | if ([mode isEqualToString:@"restore"]) {
76 | if ([originalSets objectForKey:key]){
77 | proxies = originalSets[key][@"Proxies"];
78 | }
79 | }
80 |
81 | if ([mode isEqualToString:@"auto"]) {
82 |
83 | [proxies setObject:@"http://127.0.0.1:8070/proxy.pac" forKey:(NSString *)kCFNetworkProxiesProxyAutoConfigURLString];
84 | [proxies setObject:[NSNumber numberWithInt:1] forKey:(NSString *)kCFNetworkProxiesProxyAutoConfigEnable];
85 |
86 | } else if ([mode isEqualToString:@"global"]) {
87 | int localPort = 0;
88 | int httpPort = 0;
89 | if (sscanf (argv[2], "%i", &localPort)!=1 || localPort > 65535 || localPort < 0) {
90 | printf ("error - not a valid port number");
91 | return 1;
92 | }
93 | if (sscanf (argv[3], "%i", &httpPort)!=1 || httpPort > 65535 || httpPort < 0) {
94 | printf ("error - not a valid port number");
95 | return 1;
96 | }
97 | NSLog(@"in helper %d %d", localPort, httpPort);
98 | if (localPort > 0) {
99 | [proxies setObject:@"127.0.0.1" forKey:(NSString *)
100 | kCFNetworkProxiesSOCKSProxy];
101 | [proxies setObject:[NSNumber numberWithInt:localPort] forKey:(NSString*)
102 | kCFNetworkProxiesSOCKSPort];
103 | [proxies setObject:[NSNumber numberWithInt:1] forKey:(NSString*)
104 | kCFNetworkProxiesSOCKSEnable];
105 | }
106 | if (httpPort > 0) {
107 | [proxies setObject:@"127.0.0.1" forKey:(NSString *)
108 | kCFNetworkProxiesHTTPProxy];
109 | [proxies setObject:@"127.0.0.1" forKey:(NSString *)
110 | kCFNetworkProxiesHTTPSProxy];
111 | [proxies setObject:[NSNumber numberWithInt:httpPort] forKey:(NSString*)
112 | kCFNetworkProxiesHTTPPort];
113 | [proxies setObject:[NSNumber numberWithInt:httpPort] forKey:(NSString*)
114 | kCFNetworkProxiesHTTPSPort];
115 | [proxies setObject:[NSNumber numberWithInt:1] forKey:(NSString*)
116 | kCFNetworkProxiesHTTPEnable];
117 | [proxies setObject:[NSNumber numberWithInt:1] forKey:(NSString*)
118 | kCFNetworkProxiesHTTPSEnable];
119 | }
120 | }
121 |
122 | SCPreferencesPathSetValue(prefRef, (__bridge CFStringRef)[NSString stringWithFormat:@"/%@/%@/%@", kSCPrefNetworkServices, key, kSCEntNetProxies], (__bridge CFDictionaryRef)proxies);
123 | }
124 | }
125 |
126 | SCPreferencesCommitChanges(prefRef);
127 | SCPreferencesApplyChanges(prefRef);
128 | SCPreferencesSynchronize(prefRef);
129 |
130 | }
131 |
132 | printf("proxy set to %s\n", [mode UTF8String]);
133 | }
134 |
135 | return 0;
136 | }
137 |
--------------------------------------------------------------------------------
/v2rayx_sysconf/sysconf_version.h:
--------------------------------------------------------------------------------
1 | //
2 | // sysconf_version.h
3 | // V2RayX
4 | //
5 | //
6 |
7 | #ifndef sysconf_version_h
8 | #define sysconf_version_h
9 |
10 |
11 | #define VERSION @"v2rayx_sysconf 1.4.0"
12 |
13 | #endif /* sysconf_version_h */
14 |
--------------------------------------------------------------------------------
/vx.ggb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cenmrev/V2RayX/3551c3eb496d403a96c7bd5ef8c31988636e4a4f/vx.ggb
--------------------------------------------------------------------------------