287 Commits

Author SHA1 Message Date
René Preuß
814eb964d5 Fork expose 2022-07-07 22:27:23 +02:00
Marcel Pociot
da82980286 Fix PHP8.1 issue in 2.2.1 2022-07-04 13:46:51 +02:00
Marcel Pociot
9f03b8e5ac Add basic auth to cwd sharing, bump version 2022-07-04 13:29:21 +02:00
Marcel Pociot
2d3b10b63d Fix regression issue and readd basic auth support (#328) 2022-07-04 13:26:21 +02:00
Marcel Pociot
5d99a0d7d8 bump 2022-03-17 12:06:23 +01:00
Marcel Pociot
ecacc69d34 Merge branch 'master' of github.com:beyondcode/expose 2022-03-17 12:05:45 +01:00
Marcel Pociot
729d1fc817 Fixes #288 2022-03-17 12:05:38 +01:00
Marcel Pociot
177088dcf5 Merge pull request #312 from beyondcode/analysis-QMG5rw
Apply fixes from StyleCI
2022-03-17 11:58:42 +01:00
StyleCI Bot
c44f14d28a Apply fixes from StyleCI 2022-03-17 10:58:36 +00:00
Marcel Pociot
e079a6320c Remove internal dashboard link for performance reasons 2022-03-17 11:58:10 +01:00
Marcel Pociot
c828c3c0d2 Bump version 2022-03-14 10:18:15 +01:00
Marcel Pociot
9aa6d5d3f9 wip 2022-03-04 17:44:48 +01:00
Marcel Pociot
119d0826e4 Merge branch 'master' of github.com:beyondcode/expose 2022-03-04 17:35:57 +01:00
Marcel Pociot
c2b0b62a8b Post connection info and user to endpoint 2022-03-04 17:35:20 +01:00
Marcel Pociot
428356badb Merge pull request #309 from beyondcode/analysis-VrJ6R3
Apply fixes from StyleCI
2022-03-04 15:53:46 +01:00
StyleCI Bot
e375d21e4c Apply fixes from StyleCI 2022-03-04 14:53:40 +00:00
Marcel Pociot
4e2eda036a Added the ability to retrieve welcome messags from APIs 2022-03-04 14:06:13 +01:00
Marcel Pociot
7fadb687cc Allow specifying local config files when starting the server 2022-03-04 12:21:44 +01:00
Marcel Pociot
3cc290998d wip 2022-03-04 12:05:28 +01:00
Marcel Pociot
117424cf0e wip 2022-02-24 12:58:39 +01:00
Marcel Pociot
83f49d49c2 Merge branch 'master' into update-cli-output 2022-02-23 17:39:03 +01:00
Marcel Pociot
26541d4af9 Merge branch 'master' of github.com:beyondcode/expose 2022-02-23 17:35:02 +01:00
Marcel Pociot
00b379c417 Use new phar updater dependency 2022-02-23 17:34:58 +01:00
Marcel Pociot
1f09672b51 Merge pull request #307 from beyondcode/analysis-9mEOD5
Apply fixes from StyleCI
2022-02-23 12:44:26 +01:00
StyleCI Bot
cd625e4840 Apply fixes from StyleCI 2022-02-23 11:44:20 +00:00
Marcel Pociot
813f742c20 Fix redirect issue when using custom domains 2022-02-23 12:43:54 +01:00
Marcel Pociot
89c9fa6742 wip 2022-02-23 12:38:42 +01:00
Markus Lilienberg
3aa4847d33 Always return '$this' in 'registerStatisticsCollector' (#292) 2022-02-12 12:08:20 +01:00
Matthieu Mota
76ce21aebb Drop php 7.4 on Docker (#300) 2022-02-12 12:07:40 +01:00
Marcel Pociot
816652e527 Remove PHP 7.4 support 2022-02-08 11:19:15 +01:00
Marcel Pociot
a199aa8576 wip 2022-02-08 11:16:57 +01:00
Marcel Pociot
92c4c2ffe1 Merge branch 'master' of github.com:beyondcode/expose 2022-02-07 13:12:27 +01:00
Marcel Pociot
9304b93775 Use Tailwind Play CDN 2022-02-07 13:12:13 +01:00
Marcel Pociot
f2793bcef9 Run tests for 8.1 2022-02-07 12:59:18 +01:00
Marcel Pociot
8b7df07b27 Merge branch 'master' of github.com:beyondcode/expose 2022-02-07 12:58:22 +01:00
Marcel Pociot
10e431cb26 PHP 8.1 compatible release 2022-02-07 12:58:07 +01:00
Sebastian Schlein
408e9e470e Update README.md 2022-02-03 12:38:01 +01:00
Marcel Pociot
9dd82bf1dc Merge pull request #294 from beyondcode/analysis-NAn06g
Apply fixes from StyleCI
2021-12-21 15:50:56 +01:00
Marcel Pociot
fd7f4ee43b Apply fixes from StyleCI 2021-12-21 14:50:50 +00:00
Marcel Pociot
22c2f090e2 Add WebHook connection callback 2021-12-21 15:50:28 +01:00
Marcel Pociot
d34f6d1300 perform connection callbacks 2021-12-21 15:37:04 +01:00
Marcel Pociot
12411c4fb5 PHP 8.1 support preparations 2021-12-16 16:41:50 +01:00
Marcel Pociot
42044f35d9 PHP 8.1 support preparations 2021-12-16 16:41:26 +01:00
Marcel Pociot
c8c47e8bf6 Allow disconnect using subdomain and server host 2021-11-26 16:37:28 +01:00
Marcel Pociot
e292b1ad3d Merge branch 'master' of github.com:beyondcode/phunnel 2021-11-10 16:47:27 +01:00
Marcel Pociot
e169a3a7c9 wip 2021-11-10 16:47:19 +01:00
Marcel Pociot
f67cc87f75 Merge pull request #286 from beyondcode/analysis-NAG0rG
Apply fixes from StyleCI
2021-11-10 16:44:09 +01:00
Marcel Pociot
70416dcb18 Apply fixes from StyleCI 2021-11-10 15:44:02 +00:00
Marcel Pociot
b9b07c9664 Add the ability to log users and their shared subdomains 2021-11-10 16:43:23 +01:00
Marcel Pociot
97993318e7 Bump version 2021-06-23 15:34:45 +02:00
Marcel Pociot
074051c4d1 Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-23 15:34:18 +02:00
Marcel Pociot
5a1f3ab2ff Disable ssl verification when resolving server endpoints 2021-06-23 15:34:14 +02:00
Phil E. Taylor
3dd0148895 s/form/div (#252) 2021-06-22 13:49:07 +02:00
Marcel Pociot
d83104567d Merge pull request #247 from beyondcode/analysis-nN72Av
Apply fixes from StyleCI
2021-06-22 10:29:00 +02:00
Marcel Pociot
9b3398db8f Apply fixes from StyleCI 2021-06-22 08:28:53 +00:00
Marcel Pociot
48c759a7d9 wip 2021-06-22 10:28:32 +02:00
Marcel Pociot
71ce328eb0 wip 2021-06-22 10:19:53 +02:00
Marcel Pociot
490365fe14 try timer 2021-06-22 10:18:31 +02:00
Sebastian Schlein
7797814ebf Merge branch 'master' of github.com:beyondcode/expose into master 2021-06-22 09:05:08 +02:00
Sebastian Schlein
e553cbb957 improve server docs 2021-06-22 09:04:22 +02:00
Marcel Pociot
19afa3cdea Update README.md 2021-06-22 09:01:36 +02:00
Marcel Pociot
90acf38b08 wip 2021-06-21 23:42:43 +02:00
Marcel Pociot
1450342fcc Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-21 23:35:28 +02:00
Marcel Pociot
4cbabeaff3 wip 2021-06-21 23:35:19 +02:00
Sebastian Schlein
1947d1daab Improve packagist description 2021-06-21 23:15:30 +02:00
Marcel Pociot
8dd1254555 Update run-tests.yml 2021-06-21 23:07:14 +02:00
Marcel Pociot
147da22c0d Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-21 23:03:21 +02:00
Marcel Pociot
cacdf1e268 wip 2021-06-21 23:02:35 +02:00
Marcel Pociot
5470fe8432 Update run-tests.yml 2021-06-21 23:00:31 +02:00
Marcel Pociot
d8f482bf57 Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-21 22:53:39 +02:00
Marcel Pociot
079c880fd1 wip 2021-06-21 22:53:33 +02:00
Marcel Pociot
780a25df91 Update run-tests.yml 2021-06-21 22:53:24 +02:00
Marcel Pociot
bdc4493ff8 wip 2021-06-21 22:47:09 +02:00
Marcel Pociot
cbe5c3014f wip 2021-06-21 22:45:45 +02:00
Marcel Pociot
d3151fd12b wip 2021-06-21 22:43:12 +02:00
Marcel Pociot
73d0421c88 Update run-tests.yml 2021-06-21 22:43:03 +02:00
Marcel Pociot
77e0b17151 wip 2021-06-21 22:35:21 +02:00
Marcel Pociot
136e435403 wip 2021-06-21 22:32:37 +02:00
Marcel Pociot
3a9d4fb6b6 wip 2021-06-21 22:28:52 +02:00
Marcel Pociot
c81cca6e0e Update run-tests.yml 2021-06-21 22:21:48 +02:00
Marcel Pociot
856163e267 Create run-tests.yml 2021-06-21 22:18:21 +02:00
Sebastian Schlein
c2845a3e13 Merge branch 'master' of github.com:beyondcode/expose into master 2021-06-21 21:55:07 +02:00
Sebastian Schlein
8d9500abeb improve docs 2021-06-21 21:54:46 +02:00
Marcel Pociot
bbeaa1f0f1 wip 2021-06-21 21:50:45 +02:00
Marcel Pociot
f72c0d546b wip 2021-06-21 21:31:35 +02:00
Marcel Pociot
9943132704 Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-21 16:41:40 +02:00
Marcel Pociot
1a78982dcb Update build 2021-06-21 16:41:32 +02:00
Marcel Pociot
dc29623bb4 Merge pull request #246 from beyondcode/analysis-KZ9gPj
Apply fixes from StyleCI
2021-06-21 16:34:28 +02:00
Marcel Pociot
bb87ef0adf Apply fixes from StyleCI 2021-06-21 14:34:21 +00:00
Marcel Pociot
4163975022 add docs 2021-06-21 16:33:57 +02:00
Marcel Pociot
0b07c3b2a3 wip 2021-06-21 10:12:04 +02:00
Marcel Pociot
c395ec16ae wip 2021-06-21 10:09:12 +02:00
Marcel Pociot
a33aaccc84 wip 2021-06-21 10:06:18 +02:00
Marcel Pociot
dfc26570b2 wip 2021-06-20 11:44:53 +02:00
Sebastian Schlein
19b6f35c48 Add command to list available servers (#245)
* Add command to list available servers

* Apply fixes from StyleCI

Co-authored-by: Marcel Pociot <mpociot@users.noreply.github.com>
Co-authored-by: Marcel Pociot <m.pociot@gmail.com>
2021-06-18 14:51:53 +02:00
Di
7ff697a09d Updated homepage & 404 (#243) 2021-06-18 13:23:19 +02:00
Marcel Pociot
84936ae63f Remove share files command for now 2021-06-18 13:13:51 +02:00
Marcel Pociot
c8171de2d2 send expose version when sharing tcp ports 2021-06-18 13:13:14 +02:00
Marcel Pociot
520a5afb1f wip 2021-06-17 17:56:12 +02:00
Marcel Pociot
5ad9f01e55 wip 2021-06-17 15:33:37 +02:00
Marcel Pociot
4aecb04397 Merge pull request #242 from beyondcode/analysis-YjpGoA
Apply fixes from StyleCI
2021-06-17 13:18:15 +02:00
Marcel Pociot
d22e63701f Apply fixes from StyleCI 2021-06-17 11:18:08 +00:00
Marcel Pociot
ddf99cd7c8 Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-17 13:17:39 +02:00
Marcel Pociot
19606a78af wip 2021-06-17 13:17:32 +02:00
Marcel Pociot
7742658527 Add site details api route 2021-06-17 10:59:33 +02:00
Marcel Pociot
c9cb29ed35 Pass client version to server 2021-06-16 21:00:13 +02:00
Sebastian Schlein
37349493ab improve Docker setup docs 2021-06-14 16:51:33 +02:00
Sebastian Schlein
d610705af7 Merge branch 'master' of github.com:beyondcode/expose into master 2021-06-14 16:49:35 +02:00
Sebastian Schlein
b9200e3790 improve docs in preparation for the Expose 2 launch 2021-06-14 16:49:29 +02:00
Marcel Pociot
fd66366438 Merge pull request #238 from beyondcode/analysis-1bJgkv
Apply fixes from StyleCI
2021-06-14 15:28:18 +02:00
Marcel Pociot
361f5f0b0d Apply fixes from StyleCI 2021-06-14 13:28:10 +00:00
Marcel Pociot
1d97d63d2b wip 2021-06-14 15:27:53 +02:00
Marcel Pociot
95098c180d do not follow redirects 2021-06-14 15:25:58 +02:00
Marcel Pociot
6ffd5274b3 wip 2021-06-14 13:14:20 +02:00
Marcel Pociot
7c78f7e2b1 Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-14 11:59:59 +02:00
Marcel Pociot
4d80b14551 Remove subdomain reserve check from server 2021-06-14 11:59:51 +02:00
Marcel Pociot
98482a6ce2 Merge pull request #237 from beyondcode/analysis-NA9joy
Apply fixes from StyleCI
2021-06-14 11:50:44 +02:00
Marcel Pociot
c531d41e03 Apply fixes from StyleCI 2021-06-14 09:50:37 +00:00
Marcel Pociot
1f2e21548c Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-14 11:50:11 +02:00
Marcel Pociot
aa08029fc3 Remove subdomain reserve check from server 2021-06-14 11:50:03 +02:00
Marcel Pociot
afebe13f00 Update installation.md 2021-06-11 16:23:08 +02:00
Sebastian Schlein
1fdcc50d4a Merge branch 'master' of github.com:beyondcode/expose into master 2021-06-11 16:09:48 +02:00
Sebastian Schlein
c06bcb7119 Improve docs 2021-06-11 16:09:40 +02:00
Marcel Pociot
eefd74e82c Merge pull request #234 from beyondcode/analysis-M1RaKK
Apply fixes from StyleCI
2021-06-11 15:17:21 +02:00
Marcel Pociot
de9b85df49 Apply fixes from StyleCI 2021-06-11 13:17:14 +00:00
Marcel Pociot
8be8aff802 Improve subdomain detection 2021-06-11 15:16:52 +02:00
Marcel Pociot
21a9117dd6 Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-11 12:54:44 +02:00
Marcel Pociot
351253cc19 wip 2021-06-11 12:54:39 +02:00
Marcel Pociot
6b9fee9326 Update TunnelMessageController.php 2021-06-10 21:10:50 +02:00
Marcel Pociot
2a439371be Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-10 10:15:59 +02:00
Marcel Pociot
fb05f23124 wip 2021-06-10 10:15:53 +02:00
r3vit
f565241740 Replace documentation link in home and 404 page with direct link to official docs beyondco.de (#222) 2021-06-08 12:58:45 +02:00
Matthieu Mota
8c5b52769e Fix docker entrypoint (#174) 2021-06-08 12:58:29 +02:00
Marcel Pociot
98ced10737 Merge pull request #226 from beyondcode/analysis-e7nkya
Apply fixes from StyleCI
2021-06-07 21:02:56 +02:00
Marcel Pociot
cf74165479 Apply fixes from StyleCI 2021-06-07 19:02:49 +00:00
Marcel Pociot
4131b6abb7 Add ability to specify auth tokens when creating new users 2021-06-07 21:02:26 +02:00
Marcel Pociot
8664d7ea80 Merge pull request #221 from beyondcode/analysis-gOJRbw
Apply fixes from StyleCI
2021-06-02 12:34:58 +02:00
Marcel Pociot
d4dbdba4c6 Apply fixes from StyleCI 2021-06-02 10:34:52 +00:00
Marcel Pociot
b9719ea420 wip 2021-06-02 12:34:43 +02:00
Marcel Pociot
c3896e0ba2 Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-02 12:05:49 +02:00
Marcel Pociot
078a656a21 wip 2021-06-02 12:05:44 +02:00
Marcel Pociot
e8de146dc5 Merge pull request #220 from beyondcode/analysis-vQg6BN
Apply fixes from StyleCI
2021-06-02 11:59:13 +02:00
Marcel Pociot
8d8297cf71 Apply fixes from StyleCI 2021-06-02 09:59:06 +00:00
Marcel Pociot
01843173bc wip 2021-06-02 11:58:57 +02:00
Marcel Pociot
2c90707e28 Merge pull request #219 from beyondcode/analysis-EAlD0M
Apply fixes from StyleCI
2021-06-02 10:22:09 +02:00
Marcel Pociot
01ce0d09e3 Apply fixes from StyleCI 2021-06-02 08:22:02 +00:00
Marcel Pociot
2c8804cff3 Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-02 10:21:40 +02:00
Marcel Pociot
74ac9d2d1a Custom domain support 2021-06-02 10:21:36 +02:00
Marcel Pociot
e2da5652e5 Merge pull request #218 from beyondcode/analysis-0gBW2e
Apply fixes from StyleCI
2021-06-01 21:19:44 +02:00
Marcel Pociot
6d391c9246 Apply fixes from StyleCI 2021-06-01 19:19:38 +00:00
Marcel Pociot
c23af81668 Merge branch 'master' of github.com:beyondcode/phunnel 2021-06-01 21:19:24 +02:00
Marcel Pociot
44b100b340 Added custom server host ability 2021-06-01 21:19:12 +02:00
Marcel Pociot
a83a57ca34 Merge pull request #217 from beyondcode/analysis-OM4brW
Apply fixes from StyleCI
2021-06-01 20:26:53 +02:00
Marcel Pociot
fc5ac1c53f Apply fixes from StyleCI 2021-06-01 18:26:46 +00:00
Marcel Pociot
5e54d0a80f API modifications 2021-06-01 20:26:23 +02:00
Marcel Pociot
a29874e221 Merge pull request #216 from beyondcode/analysis-pe7Rxk
Apply fixes from StyleCI
2021-05-31 16:39:44 +02:00
Marcel Pociot
6b02eafc87 Apply fixes from StyleCI 2021-05-31 14:39:37 +00:00
Marcel Pociot
9623793df5 Merge branch 'master' of github.com:beyondcode/phunnel 2021-05-31 16:39:22 +02:00
Marcel Pociot
62aa85f092 wip 2021-05-31 16:34:43 +02:00
Marcel Pociot
3de6ee1a1e Merge pull request #215 from beyondcode/analysis-vQgLjy
Apply fixes from StyleCI
2021-05-31 16:19:58 +02:00
Marcel Pociot
bbbabcebaf Apply fixes from StyleCI 2021-05-31 14:19:51 +00:00
Marcel Pociot
3c07660c2c wip 2021-05-31 16:19:43 +02:00
Marcel Pociot
21e47bde81 Merge pull request #214 from beyondcode/analysis-ajYQxW
Apply fixes from StyleCI
2021-05-31 14:48:25 +02:00
Marcel Pociot
ad2ef94958 Apply fixes from StyleCI 2021-05-31 12:48:18 +00:00
Marcel Pociot
5de11e90f7 Merge branch 'master' of github.com:beyondcode/phunnel 2021-05-31 14:47:52 +02:00
Marcel Pociot
9444d1aacb Add statistic tracking 2021-05-31 14:47:48 +02:00
Marcel Pociot
de0ada67e3 Merge pull request #212 from beyondcode/analysis-bQVQ7A
Apply fixes from StyleCI
2021-05-28 16:52:32 +02:00
Marcel Pociot
400361dd71 Apply fixes from StyleCI 2021-05-28 14:52:25 +00:00
Marcel Pociot
7f6be8cae2 Merge branch 'master' of github.com:beyondcode/phunnel 2021-05-28 16:51:58 +02:00
Marcel Pociot
a3d1735b6e Allow specifying maximum connection counts per user 2021-05-28 16:51:48 +02:00
Marcel Pociot
b44d5f9c46 Merge pull request #211 from beyondcode/analysis-lKln1Q
Apply fixes from StyleCI
2021-05-21 17:21:20 +02:00
Marcel Pociot
6c0aa790e5 Apply fixes from StyleCI 2021-05-21 15:21:13 +00:00
Marcel Pociot
717e8cf05c wip 2021-05-21 17:20:48 +02:00
Marcel Pociot
9220e83798 Merge branch 'master' of github.com:beyondcode/phunnel 2021-05-19 20:53:35 +02:00
Marcel Pociot
9e67b5ef5d Ensure that migrations are sorted properly 2021-05-19 20:52:56 +02:00
Marcel Pociot
2faacd58c5 Merge pull request #210 from beyondcode/analysis-9mYvR5
Apply fixes from StyleCI
2021-05-19 20:21:50 +02:00
Marcel Pociot
9342a5ce36 Apply fixes from StyleCI 2021-05-19 18:21:43 +00:00
Marcel Pociot
db57f83bdf Dashboard UI updates, allow multiple expose servers, exclude subdomains 2021-05-19 20:21:19 +02:00
Marcel Pociot
c1f7125f72 Get rid of deprecated method calls 2021-05-19 12:10:13 +02:00
Marcel Pociot
96fa7c653f Merge branch 'dashboard-modifications' into share-files 2021-05-19 11:57:25 +02:00
Marcel Pociot
44dca53687 Make server configurable when sharing files. Fix react/http compatibility issues 2021-05-19 11:35:27 +02:00
Marcel Pociot
60af8bce19 Merge branch 'master' into share-files 2021-05-18 12:42:28 +02:00
Marcel Pociot
60ce7816a2 wip 2021-05-18 12:39:12 +02:00
Suraj Kumar Shrestha
6e9c3503e3 fix: update port mapping (#169)
Fixes https://github.com/beyondcode/expose/issues/166
2021-03-31 07:43:15 +02:00
Dennis Koch
8d5a4410f7 feat: Config option to set default protocol to https (#110)
* feat: Config option to set default protocol to https

* style: Fix for StyleCI
2021-03-31 07:41:34 +02:00
Christian Holladay
cca03ab8b2 Update Cuzzle to 3.1 (#192) 2021-03-25 22:05:32 +01:00
Marcel Pociot
a9699eb254 Use config auth when sharing TCP ports 2021-03-05 09:14:03 +01:00
Marcel Pociot
21ed707718 Merge pull request #190 from beyondcode/analysis-0g9RRM
Apply fixes from StyleCI
2021-03-02 16:40:23 +01:00
Marcel Pociot
9687c92463 Apply fixes from StyleCI 2021-03-02 15:40:15 +00:00
Marcel Pociot
5c141986fe Allow overriding DNS server. Set DNS if internal docker host is being shared 2021-03-02 16:39:51 +01:00
Marcel Pociot
986428fe00 Add PHP 8 compatible build 2021-01-14 13:23:45 +01:00
Marcel Pociot
2934731c7a Add PHP8 compatibility to v1.x (#177)
* Add PHP8 compatible requirements
* Readd DNS to allow resolving local shared domains
2021-01-14 13:20:59 +01:00
Marcel Pociot
e2e9edf769 Fix tests 2021-01-08 21:35:00 +01:00
Vaggelis Yfantis
06c1758916 PHP 8.0 Support (#163)
* PHP v8 Support

* move namshi/cuzzle to octoper/cuzzle

* build expose binary
2021-01-08 21:29:14 +01:00
Marcel Pociot
ff232d9ef4 Merge branch '1.0' of github.com:beyondcode/phunnel into 1.0 2020-12-07 23:31:08 +01:00
Marcel Pociot
5b8cc4d985 Allow specifying server host and port in share command 2020-12-07 23:30:57 +01:00
Marcel Pociot
26a805c552 Merge pull request #164 from beyondcode/analysis-gOmLGw
Apply fixes from StyleCI
2020-12-05 00:34:31 +01:00
Marcel Pociot
28cc353c30 Apply fixes from StyleCI 2020-12-04 23:34:24 +00:00
Marcel Pociot
7bfb618d93 wip 2020-12-05 00:34:01 +01:00
Siebe Vanden Eynden
f6d04777e1 Allow custom config file path (#145)
* allow custom config file path

* Update configuration.md
2020-12-04 22:45:29 +01:00
Tii
bded9f754e Added command line options for server-host and server-port (#147)
* Added server options

* Restored box.json

* Reverted build and versioning...

* Please the style gods
2020-12-04 22:44:25 +01:00
Tii
c92d4b258c Removed fixed IP address for DNS (#148) 2020-12-04 22:39:57 +01:00
Marcel Pociot
eb8d1f4f91 Merge pull request #154 from beyondcode/analysis-5ZodwW
Apply fixes from StyleCI
2020-11-01 20:34:40 +01:00
Marcel Pociot
da39fb8ad8 Apply fixes from StyleCI 2020-11-01 19:34:33 +00:00
Marcel Pociot
5b7a80bb0c Merge branch 'master' of github.com:beyondcode/phunnel 2020-11-01 20:34:21 +01:00
Marcel Pociot
548c29772a Merge branch 'share-files' of github.com:beyondcode/phunnel into share-files 2020-11-01 20:34:06 +01:00
Marcel Pociot
844a3cd15a Rename page title 2020-11-01 20:33:56 +01:00
Marcel Pociot
f5c009eadd Merge pull request #153 from beyondcode/analysis-7ao7E3
Apply fixes from StyleCI
2020-11-01 17:47:36 +01:00
Marcel Pociot
7459c0189b Apply fixes from StyleCI 2020-11-01 16:47:29 +00:00
Merkhad Luigton
8b8426cd3b make expose directly executable in the docker container (#149) 2020-11-01 17:47:21 +01:00
Marcel Pociot
e773dfa689 Merge pull request #151 from beyondcode/analysis-64ov5W
Apply fixes from StyleCI
2020-11-01 17:44:14 +01:00
Marcel Pociot
c56f05c030 Apply fixes from StyleCI 2020-11-01 16:44:06 +00:00
Marcel Pociot
ce945e1326 Add fileserver support 2020-11-01 17:43:42 +01:00
Marcel Pociot
880259657f Rewrite location header 2020-10-25 23:40:45 +01:00
Marcel Pociot
538c7da446 Better memory management for binary responses. Fixes #140 2020-10-17 21:07:29 +02:00
Marcel Pociot
26de32d375 Allow users to reserve subdomains (#131) 2020-09-09 21:57:42 +02:00
Marcel Pociot
2f57fa1952 Add ability to expose TCP connections (#123) 2020-09-08 16:27:39 +02:00
Marcel Pociot
c8cfe7b8b4 Merge pull request #121 from beyondcode/add_flag_to_users_to_allow_custom_subdomains
Add a new flag to users to allow the specification of custom subdomains
2020-09-07 20:27:03 +02:00
Marcel Pociot
ab316f6bdc Merge pull request #117 from beyondcode/dependabot/composer/symfony/http-kernel-5.1.5
Bump symfony/http-kernel from 5.1.2 to 5.1.5
2020-09-07 20:24:17 +02:00
Marcel Pociot
b071e81b1d Merge pull request #120 from beyondcode/analysis-NAg54v
Apply fixes from StyleCI
2020-09-07 20:20:20 +02:00
Marcel Pociot
d9ab55f308 Apply fixes from StyleCI 2020-09-07 18:20:13 +00:00
Marcel Pociot
0ebe6a4ce4 Add a new flag to users to allow the specification of custom subdomains 2020-09-07 20:19:56 +02:00
Marcel Pociot
faa3309c70 Merge pull request #119 from beyondcode/associate-sites-with-auth-tokens
Associate shared sites with auth tokens
2020-09-07 13:55:52 +02:00
Marcel Pociot
a83349e6b9 Merge pull request #118 from beyondcode/analysis-gOEJOG
Apply fixes from StyleCI
2020-09-07 13:34:13 +02:00
Marcel Pociot
9363e97d81 Apply fixes from StyleCI 2020-09-07 11:34:05 +00:00
Marcel Pociot
47b2350631 Associate shared sites with auth tokens 2020-09-07 13:33:40 +02:00
dependabot[bot]
74236b6863 Bump symfony/http-kernel from 5.1.2 to 5.1.5
Bumps [symfony/http-kernel](https://github.com/symfony/http-kernel) from 5.1.2 to 5.1.5.
- [Release notes](https://github.com/symfony/http-kernel/releases)
- [Changelog](https://github.com/symfony/http-kernel/blob/master/CHANGELOG.md)
- [Commits](https://github.com/symfony/http-kernel/compare/v5.1.2...v5.1.5)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-04 14:30:06 +00:00
Marcel Pociot
b1d23e1f75 Merge pull request #112 from leemcd56/master
Removed padraic/phar-updater
2020-09-04 16:26:20 +02:00
Nathanael McDaniel
e52659bf59 Removed padraic/phar-updater 2020-08-26 14:47:01 -05:00
Marcel Pociot
13f184a955 Merge pull request #108 from beyondcode/analysis-J2x92V
Apply fixes from StyleCI
2020-08-13 00:20:14 +02:00
Marcel Pociot
55a456d5e1 Apply fixes from StyleCI 2020-08-12 22:20:06 +00:00
Marcel Pociot
f9084c3c31 Merge pull request #83 from ahmedash95/detect-valet-links
Auto detect valet links
2020-08-13 00:19:47 +02:00
Marcel Pociot
730b8457a6 Merge pull request #106 from CDRO/patch-1
[DOCS] Describe apache proxy configuration example
2020-08-07 14:34:10 +02:00
Tizian Schmidlin
188e1efe57 [DOCS] Describe apache proxy configuration exampl 2020-08-03 09:00:55 +02:00
Marcel Pociot
eaf04a8eae Don't try to log requests as curl when the requests are bigger than 256kb 2020-07-31 10:55:32 +02:00
Marcel Pociot
41e6e674e0 wip 2020-07-28 22:34:30 +02:00
Marcel Pociot
3d76b49fea wip 2020-07-28 22:23:31 +02:00
Marcel Pociot
1d5169af07 fix github packages config 2020-07-28 22:08:48 +02:00
Marcel Pociot
0945b1e66b Merge branch 'lukepolo-master' 2020-07-28 21:39:10 +02:00
Marcel Pociot
a2bdf518ab change docker hub namespace 2020-07-28 21:39:03 +02:00
Marcel Pociot
0216948d18 Merge branch 'master' of https://github.com/lukepolo/expose into lukepolo-master 2020-07-28 21:38:00 +02:00
Marcel Pociot
9e31b020b6 Merge pull request #103 from beyondcode/analysis-ZlOyRP
Apply fixes from StyleCI
2020-07-27 22:11:58 +02:00
Marcel Pociot
bf0025979e Apply fixes from StyleCI 2020-07-27 20:11:50 +00:00
Marcel Pociot
dda3cbbae5 Merge pull request #87 from localheinz/feature/normalize
Enhancement: Normalize composer.json
2020-07-27 22:11:29 +02:00
Andreas Möller
6a07859078 Enhancement: Normalize composer.json 2020-07-06 23:18:27 +02:00
Ahmed Ashraf
8db13e70af set path only if exists 2020-07-03 12:53:31 +02:00
Ahmed Ashraf
dfe889692b auto detect valet links 2020-07-03 12:50:08 +02:00
Marcel Pociot
e5b2aada2f Bump build for 1.3.0 2020-07-01 18:17:32 +02:00
Marcel Pociot
076da2c0de Use hardcoded version 2020-07-01 18:14:55 +02:00
Marcel Pociot
6410c7eb5e Merge pull request #81 from beyondcode/analysis-GDEdVA
Apply fixes from StyleCI
2020-07-01 18:11:21 +02:00
Marcel Pociot
0d9413dfdf Apply fixes from StyleCI 2020-07-01 16:11:13 +00:00
Marcel Pociot
87a4115c14 Merge pull request #79 from ahmedash95/users-pagination
Add simple pagination to users page
2020-07-01 18:10:58 +02:00
Luke Policinski
096a2b2a70 Simplified based on githubs docs
https://docs.github.com/en/actions/language-and-framework-guides/publishing-docker-images
2020-07-01 11:59:53 -04:00
Marcel Pociot
6d6306b3b2 Add request time to cli output 2020-07-01 17:58:54 +02:00
Marcel Pociot
611a4c617c Add X-Forwarded-Host header. Fixes #63 2020-07-01 17:46:54 +02:00
Marcel Pociot
54bd95c66c Merge pull request #67 from Ayesh/export-ignore
Update .gitattributes to export-ignore tests, docs, and other build files
2020-07-01 17:41:59 +02:00
Luke Policinski
3cb254e1f5 Update starting-the-server.md 2020-06-30 14:32:47 -04:00
Ahmed Ashraf
e960ffb825 Implement simple pagination 2020-06-26 12:20:01 +02:00
Marcel Pociot
459135f286 Merge pull request #74 from okaufmann/fix-remaining-time
fix remaining time calculation
2020-06-26 00:51:01 +02:00
Marcel Pociot
0efb42f989 Merge pull request #76 from riasvdv/patch-1
Don't use underscore in subdomain
2020-06-26 00:49:43 +02:00
Rias
6f04a0dfb6 Don't use underscore in subdomain
Even though it's perfectly valid, some providers (Paddle in our case) don't validate URLs with an underscore in them as valid, an easy fix is just using a dash instead.
2020-06-25 13:11:55 +02:00
Oliver Kaufmann
91f169460e fix remaining time calculation 2020-06-25 02:09:00 +02:00
Marcel Pociot
f8a6b45af7 Merge pull request #73 from clmntgr/patch-1
Update sharing.md with an https example
2020-06-24 11:22:30 +02:00
Clément Aigreault
b48dba1413 Update sharing.md with an https example 2020-06-24 11:18:20 +02:00
Marcel Pociot
8bcc7613d9 Merge pull request #71 from dakira/patch-1
Document missing nginx header config for ssl
2020-06-24 09:59:40 +02:00
Matthias Niess
9158887a60 document missing nginx header config for ssl 2020-06-24 09:54:13 +02:00
Luke Policinski
b3f2edd18c dont copy tests / .git 2020-06-23 13:03:06 -04:00
Luke Policinski
b4379ddf6d Adding action to push docker image 2020-06-23 12:44:56 -04:00
Luke Policinski
70a9666f37 Adding how to run with the readme 2020-06-23 12:37:15 -04:00
Luke Policinski
0b9f860138 adding a docker-compose file for starting a server 2020-06-23 12:28:21 -04:00
Ayesh Karunaratne
dae1851e1d Update .gitattributes to export-ignore tests, docs, and other build files 2020-06-23 04:47:03 +07:00
Marcel Pociot
70d275bb1c wip 2020-06-22 22:58:09 +02:00
Marcel Pociot
8b8c6c8e2e Fix subdomain check 2020-06-22 22:57:29 +02:00
Marcel Pociot
c5b89e1179 Add build 2020-06-22 22:51:21 +02:00
Marcel Pociot
18d67abc3f detect valet secured sites automatically 2020-06-22 22:49:19 +02:00
Marcel Pociot
38efb0b879 Merge branch 'master' of github.com:beyondcode/phunnel 2020-06-22 09:05:49 +02:00
Marcel Pociot
04c881a875 Register chrome-extension as a valid uri scheme. Fixes #50 2020-06-22 09:05:40 +02:00
Marcel Pociot
d98eabe36e Merge pull request #46 from drbyte/patch-1
Fix duplicate config section header
2020-06-19 09:34:38 +02:00
Marcel Pociot
262a1eac4a Use toArray representation of sites for the server. Fixes #44 2020-06-19 09:13:26 +02:00
Chris Brown
979bacb928 Fix config section heading 2020-06-18 12:04:50 -04:00
Chris Brown
732e0aeb3e Fix config section header 2020-06-18 12:02:41 -04:00
Marcel Pociot
2c0c544eeb Merge pull request #41 from beyondcode/analysis-KZpkkj
Apply fixes from StyleCI
2020-06-18 10:22:56 +02:00
Marcel Pociot
528d5d74e0 Apply fixes from StyleCI 2020-06-18 08:22:48 +00:00
Marcel Pociot
68200aedc4 wip 2020-06-18 10:21:28 +02:00
Marcel Pociot
8628a7e1b6 Update build to 1.1.0 2020-06-18 10:18:06 +02:00
Marcel Pociot
5df98c4b91 Update changelog 2020-06-18 10:17:13 +02:00
Marcel Pociot
78fbef90cd Merge pull request #40 from beyondcode/analysis-lKY3aQ
Apply fixes from StyleCI
2020-06-18 10:10:45 +02:00
146 changed files with 7016 additions and 9694 deletions

2
.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
tests
.git

4
.env-example Normal file
View File

@@ -0,0 +1,4 @@
PORT=8080
DOMAIN=example.com
ADMIN_USERNAME=username
ADMIN_PASSWORD=password

17
.gitattributes vendored
View File

@@ -1,7 +1,14 @@
* text=auto * text=auto
/.github export-ignore
.styleci.yml export-ignore /.github export-ignore
/tests export-ignore
/docs export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.styleci.yml export-ignore
.scrutinizer.yml export-ignore .scrutinizer.yml export-ignore
BACKERS.md export-ignore BACKERS.md export-ignore
CONTRIBUTING.md export-ignore CONTRIBUTING.md export-ignore
CHANGELOG.md export-ignore CHANGELOG.md export-ignore
nodemod.json export-ignore
phpunit.xml.dist export-ignore

19
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Publish Docker image
on:
release:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Push to Docker Hub
uses: docker/build-push-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: beyondcodegmbh/expose-server
tag_with_ref: true
tags: latest

36
.github/workflows/run-tests.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest]
php: [8.0, 8.1]
stability: [prefer-stable]
name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
coverage: none
- name: Setup problem matchers
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install dependencies
run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction
- name: Execute tests
run: vendor/bin/phpunit

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@
expose.php expose.php
database/expose.db database/expose.db
.expose.php .expose.php
.env

22
CHANGELOG.md Normal file
View File

@@ -0,0 +1,22 @@
# Changelog
## 1.3.0 (2020-07-01)
* Feature: Add pagination to admin user interface
* Feature: Add request time to CLI output
* Feature: Add `X-Forwarded-Host` header
* Fix: Fix remaining time calculation
* Fix: Don't use underscores for automatic subdomain generation
## 1.1.0 (2020-06-18)
* Feature: Allow overriding the subdomain when using `expose` without specifying `expose share` explicitly
* Show badges in the local dashboard for 3xx response statuses
* Fix: Updated minimum PHP dependency
* Fix: Added support for detecting the Windows user home path
* Fix: Use minified VueJS versions
* Various spelling fixes
## 1.0.1 (2020-06-17)
* Fixes an issue when setting the auth token
## 1.0.0 (2020-06-17)
* Initial release

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM php:8.0-cli
RUN apt-get update
RUN apt-get install -y git libzip-dev zip
RUN docker-php-ext-install zip
# Get latest Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY . /src
WORKDIR /src
# install the dependencies
RUN composer install -o --prefer-dist && chmod a+x expose
ENV port=8080
ENV domain=localhost
ENV username=username
ENV password=password
ENV exposeConfigPath=/src/config/expose.php
COPY docker-entrypoint.sh /usr/bin/
RUN chmod 755 /usr/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]

View File

@@ -1,4 +1,4 @@
![](https://beyondco.de/img/docs/expose/img/card.png) ![](https://expose.dev/images/expose/og_card.png)
# Expose # Expose
@@ -6,11 +6,17 @@
[![Quality Score](https://img.shields.io/scrutinizer/g/beyondcode/expose.svg?style=flat-square)](https://scrutinizer-ci.com/g/beyondcode/expose) [![Quality Score](https://img.shields.io/scrutinizer/g/beyondcode/expose.svg?style=flat-square)](https://scrutinizer-ci.com/g/beyondcode/expose)
[![Total Downloads](https://img.shields.io/packagist/dt/beyondcode/expose.svg?style=flat-square)](https://packagist.org/packages/beyondcode/expose) [![Total Downloads](https://img.shields.io/packagist/dt/beyondcode/expose.svg?style=flat-square)](https://packagist.org/packages/beyondcode/expose)
A completely open-source ngrok alternative - written in pure PHP. An open-source ngrok alternative - written in PHP.
## ⭐️ Managed Expose & Expose Pro ⭐️
You can use a managed version with our proprietary platform and our free (EU) test server at the [official website](https://expose.dev). Upgrade to Expose Pro to use our global server network with your own custom domains and get high-speed tunnels all over the world.
[Create an account](https://expose.dev)
## Documentation ## Documentation
For installation instructions, in-depth usage and deployment details, please take a look at the [official documentation](https://beyondco.de/docs/expose/). For installation instructions of your own server, in-depth usage and deployment details, please take a look at the [official documentation](https://expose.dev/docs).
### Security ### Security

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Client\Callbacks;
use App\Server\Connections\ControlConnection;
use Clue\React\Buzz\Browser;
class WebHookConnectionCallback
{
/** @var Browser */
protected $browser;
public function __construct(Browser $browser)
{
$this->browser = $browser;
}
public function handle(ControlConnection $connection)
{
$this->browser->post(config('expose.admin.connection_callbacks.webhook.url'), [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'X-Signature' => $this->generateWebhookSigningSecret($connection),
], json_encode($connection->toArray()));
}
protected function generateWebhookSigningSecret(ControlConnection $connection)
{
return hash_hmac('sha256', $connection->client_id, config('expose.admin.connection_callbacks.webhook.secret'));
}
}

View File

@@ -31,6 +31,10 @@ class Client
/** @var int */ /** @var int */
protected $timeConnected = 0; protected $timeConnected = 0;
/** @var bool */
protected $shouldExit = true;
public static $user = [];
public static $subdomains = []; public static $subdomains = [];
public function __construct(LoopInterface $loop, Configuration $configuration, CliRequestLogger $logger) public function __construct(LoopInterface $loop, Configuration $configuration, CliRequestLogger $logger)
@@ -40,15 +44,25 @@ class Client
$this->logger = $logger; $this->logger = $logger;
} }
public function share(string $sharedUrl, array $subdomains = []) public function shouldExit($shouldExit = true)
{
$this->shouldExit = $shouldExit;
}
public function share(string $sharedUrl, array $subdomains = [], $serverHost = null)
{ {
$sharedUrl = $this->prepareSharedUrl($sharedUrl); $sharedUrl = $this->prepareSharedUrl($sharedUrl);
foreach ($subdomains as $subdomain) { foreach ($subdomains as $subdomain) {
$this->connectToServer($sharedUrl, $subdomain, config('expose.auth_token')); $this->connectToServer($sharedUrl, $subdomain, $serverHost, $this->configuration->auth());
} }
} }
public function sharePort(int $port)
{
$this->connectToServerAndShareTcp($port, $this->configuration->auth());
}
protected function prepareSharedUrl(string $sharedUrl): string protected function prepareSharedUrl(string $sharedUrl): string
{ {
if (! $parsedUrl = parse_url($sharedUrl)) { if (! $parsedUrl = parse_url($sharedUrl)) {
@@ -67,34 +81,34 @@ class Client
return $url; return $url;
} }
public function connectToServer(string $sharedUrl, $subdomain, $authToken = ''): PromiseInterface public function connectToServer(string $sharedUrl, $subdomain, $serverHost = null, $authToken = ''): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$promise = $deferred->promise(); $promise = $deferred->promise();
$exposeVersion = config('app.version');
$wsProtocol = $this->configuration->port() === 443 ? 'wss' : 'ws'; $wsProtocol = $this->configuration->port() === 443 ? 'wss' : 'ws';
connect($wsProtocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control?authToken={$authToken}", [], [ connect($wsProtocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control?authToken={$authToken}&version={$exposeVersion}", [], [
'X-Expose-Control' => 'enabled', 'X-Expose-Control' => 'enabled',
], $this->loop) ], $this->loop)
->then(function (WebSocket $clientConnection) use ($sharedUrl, $subdomain, $deferred, $authToken) { ->then(function (WebSocket $clientConnection) use ($sharedUrl, $subdomain, $serverHost, $deferred, $authToken) {
$this->connectionRetries = 0; $this->connectionRetries = 0;
$connection = ControlConnection::create($clientConnection); $connection = ControlConnection::create($clientConnection);
$connection->authenticate($sharedUrl, $subdomain); $connection->authenticate($sharedUrl, $subdomain, $serverHost);
$clientConnection->on('close', function () use ($deferred, $sharedUrl, $subdomain, $authToken) { $clientConnection->on('close', function () use ($sharedUrl, $subdomain, $serverHost, $authToken) {
$this->logger->error('Connection to server closed.'); $this->logger->error('Connection to server closed.');
$this->retryConnectionOrExit($sharedUrl, $subdomain, $authToken); $this->retryConnectionOrExit(function () use ($sharedUrl, $subdomain, $serverHost, $authToken) {
$this->connectToServer($sharedUrl, $subdomain, $serverHost, $authToken);
});
}); });
$connection->on('authenticationFailed', function ($data) use ($deferred) { $this->attachCommonConnectionListeners($connection, $deferred);
$this->logger->error($data->message);
$this->exit($deferred);
});
$connection->on('subdomainTaken', function ($data) use ($deferred) { $connection->on('subdomainTaken', function ($data) use ($deferred) {
$this->logger->error($data->message); $this->logger->error($data->message);
@@ -102,40 +116,32 @@ class Client
$this->exit($deferred); $this->exit($deferred);
}); });
$connection->on('setMaximumConnectionLength', function ($data) {
$timeoutSection = $this->logger->getOutput()->section();
$this->loop->addPeriodicTimer(1, function () use ($data, $timeoutSection) {
$this->timeConnected++;
$carbon = Carbon::createFromFormat('s', str_pad($data->length * 60 - $this->timeConnected, 2, 0, STR_PAD_LEFT));
$timeoutSection->clear();
$timeoutSection->writeln('Remaining time: '.$carbon->format('H:i:s'));
});
});
$connection->on('authenticated', function ($data) use ($deferred, $sharedUrl) { $connection->on('authenticated', function ($data) use ($deferred, $sharedUrl) {
$httpProtocol = $this->configuration->port() === 443 ? 'https' : 'http'; $httpProtocol = $this->configuration->port() === 443 ? 'https' : 'http';
$host = $this->configuration->host();
if ($httpProtocol !== 'https') { $httpPort = $httpProtocol === 'https' ? '' : ":{$this->configuration->port()}";
$host .= ":{$this->configuration->port()}";
} $host = $data->server_host ?? $this->configuration->host();
$this->configuration->setServerHost($host);
$this->logger->info($data->message); $this->logger->info($data->message);
$this->logger->info("Local-URL:\t\t{$sharedUrl}"); $this->logger->info("Shared URL:\t\t<options=bold>{$sharedUrl}</>");
$this->logger->info("Dashboard-URL:\t\thttp://127.0.0.1:".config()->get('expose.dashboard_port')); $this->logger->info("Dashboard:\t\t<options=bold>http://127.0.0.1:".config()->get('expose.dashboard_port').'</>');
$this->logger->info("Expose-URL:\t\t{$httpProtocol}://{$data->subdomain}.{$host}"); $this->logger->info("Public HTTP:\t\t<options=bold>http://{$data->subdomain}.{$host}{$httpPort}</>");
$this->logger->info("Public HTTPS:\t\t<options=bold>https://{$data->subdomain}.{$host}</>");
$this->logger->line(''); $this->logger->line('');
static::$subdomains[] = "{$httpProtocol}://{$data->subdomain}.{$host}"; static::$subdomains[] = "{$httpProtocol}://{$data->subdomain}.{$host}";
static::$user = $data->user ?? ['can_specify_subdomains' => 0];
$deferred->resolve($data); $deferred->resolve($data);
}); });
}, function (\Exception $e) use ($deferred, $sharedUrl, $subdomain, $authToken) { }, function (\Exception $e) use ($deferred, $sharedUrl, $subdomain, $authToken) {
if ($this->connectionRetries > 0) { if ($this->connectionRetries > 0) {
$this->retryConnectionOrExit($sharedUrl, $subdomain, $authToken); $this->retryConnectionOrExit(function () use ($sharedUrl, $subdomain, $authToken) {
$this->connectToServer($sharedUrl, $subdomain, $authToken);
});
return; return;
} }
@@ -148,24 +154,117 @@ class Client
return $promise; return $promise;
} }
public function connectToServerAndShareTcp(int $port, $authToken = ''): PromiseInterface
{
$deferred = new Deferred();
$promise = $deferred->promise();
$wsProtocol = $this->configuration->port() === 443 ? 'wss' : 'ws';
$exposeVersion = config('app.version');
connect($wsProtocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control?authToken={$authToken}&version={$exposeVersion}", [], [
'X-Expose-Control' => 'enabled',
], $this->loop)
->then(function (WebSocket $clientConnection) use ($port, $deferred, $authToken) {
$this->connectionRetries = 0;
$connection = ControlConnection::create($clientConnection);
$connection->authenticateTcp($port);
$this->attachCommonConnectionListeners($connection, $deferred);
$clientConnection->on('close', function () use ($port, $authToken) {
$this->logger->error('Connection to server closed.');
$this->retryConnectionOrExit(function () use ($port, $authToken) {
$this->connectToServerAndShareTcp($port, $authToken);
});
});
$connection->on('authenticated', function ($data) use ($deferred, $port) {
$host = $this->configuration->host();
$this->logger->info($data->message);
$this->logger->info("Local-Port:\t\t<options=bold>{$port}</>");
$this->logger->info("Shared-Port:\t\t<options=bold>{$data->shared_port}</>");
$this->logger->info("Expose-URL:\t\t<options=bold>tcp://{$host}:{$data->shared_port}</>");
$this->logger->line('');
$deferred->resolve($data);
});
}, function (\Exception $e) use ($deferred, $port, $authToken) {
if ($this->connectionRetries > 0) {
$this->retryConnectionOrExit(function () use ($port, $authToken) {
$this->connectToServerAndShareTcp($port, $authToken);
});
return;
}
$this->logger->error('Could not connect to the server.');
$this->logger->error($e->getMessage());
$this->exit($deferred);
});
return $promise;
}
protected function attachCommonConnectionListeners(ControlConnection $connection, Deferred $deferred)
{
$connection->on('info', function ($data) {
$this->logger->info($data->message);
});
$connection->on('warning', function ($data) {
$this->logger->warn($data->message);
});
$connection->on('error', function ($data) {
$this->logger->error($data->message);
});
$connection->on('authenticationFailed', function ($data) use ($deferred) {
$this->logger->error($data->message);
$this->exit($deferred);
});
$connection->on('setMaximumConnectionLength', function ($data) {
$timeoutSection = $this->logger->getOutput()->section();
$this->loop->addPeriodicTimer(1, function () use ($data, $timeoutSection) {
$this->timeConnected++;
$secondsRemaining = $data->length * 60 - $this->timeConnected;
$remaining = Carbon::now()->diff(Carbon::now()->addSeconds($secondsRemaining));
$timeoutSection->clear();
$timeoutSection->writeln('Remaining time: '.$remaining->format('%H:%I:%S'));
});
});
}
protected function exit(Deferred $deferred) protected function exit(Deferred $deferred)
{ {
$deferred->reject(); $deferred->reject();
$this->loop->futureTick(function () { $this->loop->futureTick(function () {
exit(1); if ($this->shouldExit) {
exit(1);
}
}); });
} }
protected function retryConnectionOrExit(string $sharedUrl, $subdomain, $authToken = '') protected function retryConnectionOrExit(callable $retry)
{ {
$this->connectionRetries++; $this->connectionRetries++;
if ($this->connectionRetries <= static::MAX_CONNECTION_RETRIES) { if ($this->connectionRetries <= static::MAX_CONNECTION_RETRIES) {
$this->loop->addTimer($this->connectionRetries, function () use ($sharedUrl, $subdomain, $authToken) { $this->loop->addTimer($this->connectionRetries, function () use ($retry) {
$this->logger->info("Retrying connection ({$this->connectionRetries}/".static::MAX_CONNECTION_RETRIES.')'); $this->logger->info("Retrying connection ({$this->connectionRetries}/".static::MAX_CONNECTION_RETRIES.')');
$this->connectToServer($sharedUrl, $subdomain, $authToken); $retry();
}); });
} else { } else {
exit(1); exit(1);

View File

@@ -7,19 +7,27 @@ class Configuration
/** @var string */ /** @var string */
protected $host; protected $host;
/** @var string */
protected $serverHost;
/** @var int */ /** @var int */
protected $port; protected $port;
/** @var string|null */ /** @var string|null */
protected $auth; protected $auth;
public function __construct(string $host, int $port, ?string $auth = null) /** @var string|null */
protected $basicAuth;
public function __construct(string $host, int $port, ?string $auth = null, ?string $basicAuth = null)
{ {
$this->host = $host; $this->serverHost = $this->host = $host;
$this->port = $port; $this->port = $port;
$this->auth = $auth; $this->auth = $auth;
$this->basicAuth = $basicAuth;
} }
public function host(): string public function host(): string
@@ -27,13 +35,40 @@ class Configuration
return $this->host; return $this->host;
} }
public function serverHost(): string
{
return $this->serverHost;
}
public function setServerHost($serverHost)
{
$this->serverHost = $serverHost;
}
public function auth(): ?string public function auth(): ?string
{ {
return $this->auth; return $this->auth;
} }
public function basicAuth(): ?string
{
return $this->basicAuth;
}
public function port(): int public function port(): int
{ {
return intval($this->port); return intval($this->port);
} }
public function getUrl(string $subdomain): string
{
$httpProtocol = $this->port() === 443 ? 'https' : 'http';
$host = $this->serverHost();
if ($httpProtocol !== 'https') {
$host .= ":{$this->port()}";
}
return "{$subdomain}.{$host}";
}
} }

View File

@@ -52,17 +52,35 @@ class ControlConnection
$this->proxyManager->createProxy($this->clientId, $data); $this->proxyManager->createProxy($this->clientId, $data);
} }
public function authenticate(string $sharedHost, string $subdomain) public function createTcpProxy($data)
{
$this->proxyManager->createTcpProxy($this->clientId, $data);
}
public function authenticate(string $sharedHost, string $subdomain, $serverHost = null)
{ {
$this->socket->send(json_encode([ $this->socket->send(json_encode([
'event' => 'authenticate', 'event' => 'authenticate',
'data' => [ 'data' => [
'type' => 'http',
'host' => $sharedHost, 'host' => $sharedHost,
'server_host' => $serverHost,
'subdomain' => empty($subdomain) ? null : $subdomain, 'subdomain' => empty($subdomain) ? null : $subdomain,
], ],
])); ]));
} }
public function authenticateTcp(int $port)
{
$this->socket->send(json_encode([
'event' => 'authenticate',
'data' => [
'type' => 'tcp',
'port' => $port,
],
]));
}
public function ping() public function ping()
{ {
$this->socket->send(json_encode([ $this->socket->send(json_encode([

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Client\Exceptions;
class InvalidServerProvided extends \Exception
{
public function __construct($server)
{
$message = "No such server {$server}.";
parent::__construct($message);
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Client; namespace App\Client;
use App\Client\Fileserver\Fileserver;
use App\Client\Http\Controllers\AttachDataToLogController; use App\Client\Http\Controllers\AttachDataToLogController;
use App\Client\Http\Controllers\ClearLogsController; use App\Client\Http\Controllers\ClearLogsController;
use App\Client\Http\Controllers\CreateTunnelController; use App\Client\Http\Controllers\CreateTunnelController;
@@ -27,12 +28,18 @@ class Factory
/** @var string */ /** @var string */
protected $auth = ''; protected $auth = '';
/** @var string */
protected $basicAuth;
/** @var \React\EventLoop\LoopInterface */ /** @var \React\EventLoop\LoopInterface */
protected $loop; protected $loop;
/** @var App */ /** @var App */
protected $app; protected $app;
/** @var Fileserver */
protected $fileserver;
/** @var RouteGenerator */ /** @var RouteGenerator */
protected $router; protected $router;
@@ -63,6 +70,13 @@ class Factory
return $this; return $this;
} }
public function setBasicAuth(?string $basicAuth)
{
$this->basicAuth = $basicAuth;
return $this;
}
public function setLoop(LoopInterface $loop) public function setLoop(LoopInterface $loop)
{ {
$this->loop = $loop; $this->loop = $loop;
@@ -73,7 +87,7 @@ class Factory
protected function bindConfiguration() protected function bindConfiguration()
{ {
app()->singleton(Configuration::class, function ($app) { app()->singleton(Configuration::class, function ($app) {
return new Configuration($this->host, $this->port, $this->auth); return new Configuration($this->host, $this->port, $this->auth, $this->basicAuth);
}); });
} }
@@ -102,9 +116,25 @@ class Factory
return $this; return $this;
} }
public function share($sharedUrl, $subdomain = null) public function share($sharedUrl, $subdomain = null, $serverHost = null)
{ {
app('expose.client')->share($sharedUrl, $subdomain); app('expose.client')->share($sharedUrl, $subdomain, $serverHost);
return $this;
}
public function sharePort(int $port)
{
app('expose.client')->sharePort($port);
return $this;
}
public function shareFolder(string $folder, string $name, $subdomain = null, $serverHost = null)
{
$host = $this->createFileServer($folder, $name);
$this->share($host, $subdomain, $serverHost);
return $this; return $this;
} }
@@ -120,40 +150,54 @@ class Factory
$this->router->post('/api/logs/{request_id}/data', AttachDataToLogController::class); $this->router->post('/api/logs/{request_id}/data', AttachDataToLogController::class);
$this->router->get('/api/logs/clear', ClearLogsController::class); $this->router->get('/api/logs/clear', ClearLogsController::class);
$this->app->route('/socket', new WsServer(new Socket()), ['*']); $this->app->route('/socket', new WsServer(new Socket()), ['*'], '');
foreach ($this->router->getRoutes()->all() as $name => $route) { foreach ($this->router->getRoutes()->all() as $name => $route) {
$this->app->routes->add($name, $route); $this->app->routes->add($name, $route);
} }
} }
protected function detectNextFreeDashboardPort($port = 4040): int protected function detectNextAvailablePort($startPort = 4040): int
{ {
while (is_resource(@fsockopen('127.0.0.1', $port))) { while (is_resource(@fsockopen('127.0.0.1', $startPort))) {
$port++; $startPort++;
} }
return $port; return $startPort;
} }
public function createHttpServer() public function createHttpServer()
{ {
$dashboardPort = $this->detectNextFreeDashboardPort(); $dashboardPort = $this->detectNextAvailablePort();
config()->set('expose.dashboard_port', $dashboardPort); config()->set('expose.dashboard_port', $dashboardPort);
$this->app = new App('127.0.0.1', $dashboardPort, '0.0.0.0', $this->loop); $this->app = new App('0.0.0.0', $dashboardPort, '0.0.0.0', $this->loop);
$this->addRoutes(); $this->addRoutes();
return $this; return $this;
} }
public function createFileServer(string $folder, string $name)
{
$port = $this->detectNextAvailablePort(8090);
$this->fileserver = new Fileserver($folder, $name, $port, '0.0.0.0', $this->loop);
return "127.0.0.1:{$port}";
}
public function getApp(): App public function getApp(): App
{ {
return $this->app; return $this->app;
} }
public function getFileserver(): Fileserver
{
return $this->fileserver;
}
public function run() public function run()
{ {
$this->loop->run(); $this->loop->run();

View File

@@ -0,0 +1,136 @@
<?php
namespace App\Client\Fileserver;
use App\Http\Controllers\Concerns\LoadsViews;
use App\Http\QueryParameters;
use GuzzleHttp\Psr7\ServerRequest;
use Illuminate\Http\Request;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\LoopInterface;
use React\Http\Message\Response;
use React\Stream\ReadableResourceStream;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\Iterator\FilenameFilterIterator;
class ConnectionHandler
{
use LoadsViews;
/** @var string */
protected $rootFolder;
/** @var string */
protected $name;
/** @var LoopInterface */
protected $loop;
public function __construct(string $rootFolder, string $name, LoopInterface $loop)
{
$this->rootFolder = $rootFolder;
$this->name = $name;
$this->loop = $loop;
}
public function handle(ServerRequestInterface $request)
{
$request = $this->createLaravelRequest($request);
$targetPath = realpath($this->rootFolder.DIRECTORY_SEPARATOR.$request->path());
if (! $this->isValidTarget($targetPath)) {
return new Response(404);
}
if (is_dir($targetPath)) {
// Directory listing
$directoryContent = Finder::create()
->depth(0)
->sort(function ($a, $b) {
return strcmp(strtolower($a->getRealpath()), strtolower($b->getRealpath()));
})
->in($targetPath);
if ($this->name !== '') {
$directoryContent->name($this->name);
}
$parentPath = explode('/', $request->path());
array_pop($parentPath);
$parentPath = implode('/', $parentPath);
return new Response(
200,
['Content-Type' => 'text/html'],
$this->getView(null, 'client.fileserver', [
'currentPath' => $request->path(),
'parentPath' => $parentPath,
'directory' => $targetPath,
'directoryContent' => $directoryContent,
])
);
}
if (is_file($targetPath)) {
return new Response(
200,
['Content-Type' => mime_content_type($targetPath)],
new ReadableResourceStream(fopen($targetPath, 'r'), $this->loop)
);
}
}
protected function isValidTarget(string $targetPath): bool
{
if (! file_exists($targetPath)) {
return false;
}
if ($this->name !== '') {
$filter = new class(basename($targetPath), [$this->name]) extends FilenameFilterIterator
{
protected $filename;
public function __construct(string $filename, array $matchPatterns)
{
$this->filename = $filename;
foreach ($matchPatterns as $pattern) {
$this->matchRegexps[] = $this->toRegex($pattern);
}
}
public function accept()
{
return $this->isAccepted($this->filename);
}
};
return $filter->accept();
}
return true;
}
protected function createLaravelRequest(ServerRequestInterface $request): Request
{
try {
parse_str($request->getBody(), $bodyParameters);
} catch (\Throwable $e) {
$bodyParameters = [];
}
$serverRequest = (new ServerRequest(
$request->getMethod(),
$request->getUri(),
$request->getHeaders(),
$request->getBody(),
$request->getProtocolVersion(),
))
->withQueryParams(QueryParameters::create($request)->all())
->withParsedBody($bodyParameters);
return Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest));
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Client\Fileserver;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\LoopInterface;
use React\Http\Server;
use React\Socket\Server as SocketServer;
class Fileserver
{
/** @var SocketServer */
protected $socket;
public function __construct($rootFolder, $name, $port, $address, LoopInterface $loop)
{
$server = new Server($loop, function (ServerRequestInterface $request) use ($rootFolder, $name, $loop) {
return (new ConnectionHandler($rootFolder, $name, $loop))->handle($request);
});
$this->socket = new SocketServer("{$address}:{$port}", $loop);
$server->listen($this->socket);
}
public function getSocket(): SocketServer
{
return $this->socket;
}
}

View File

@@ -4,8 +4,8 @@ namespace App\Client\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Logger\RequestLogger; use App\Logger\RequestLogger;
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use function GuzzleHttp\Psr7\str;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
@@ -28,11 +28,11 @@ class AttachDataToLogController extends Controller
$this->requestLogger->pushLoggedRequest($loggedRequest); $this->requestLogger->pushLoggedRequest($loggedRequest);
$httpConnection->send(str(new Response(200))); $httpConnection->send(Message::toString(new Response(200)));
return; return;
} }
$httpConnection->send(str(new Response(404))); $httpConnection->send(Message::toString(new Response(404)));
} }
} }

View File

@@ -3,8 +3,8 @@
namespace App\Client\Http\Controllers; namespace App\Client\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use function GuzzleHttp\Psr7\str;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
@@ -20,7 +20,7 @@ class CreateTunnelController extends Controller
$httpConnection->send(respond_json($data)); $httpConnection->send(respond_json($data));
$httpConnection->close(); $httpConnection->close();
}, function () use ($httpConnection) { }, function () use ($httpConnection) {
$httpConnection->send(str(new Response(500))); $httpConnection->send(Message::toString(new Response(500)));
$httpConnection->close(); $httpConnection->close();
}); });
} }

View File

@@ -12,6 +12,7 @@ class DashboardController extends Controller
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
{ {
$httpConnection->send(respond_html($this->getView($httpConnection, 'client.dashboard', [ $httpConnection->send(respond_html($this->getView($httpConnection, 'client.dashboard', [
'user' => Client::$user,
'subdomains' => Client::$subdomains, 'subdomains' => Client::$subdomains,
'max_logs'=> config()->get('expose.max_logged_requests', 10), 'max_logs'=> config()->get('expose.max_logged_requests', 10),
]))); ])));

View File

@@ -5,8 +5,8 @@ namespace App\Client\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\WebSockets\Socket; use App\WebSockets\Socket;
use Exception; use Exception;
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use function GuzzleHttp\Psr7\str;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
@@ -23,9 +23,9 @@ class PushLogsToDashboardController extends Controller
$webSocketConnection->send($request->getContent()); $webSocketConnection->send($request->getContent());
} }
$httpConnection->send(str(new Response(200))); $httpConnection->send(Message::toString(new Response(200)));
} catch (Exception $e) { } catch (Exception $e) {
$httpConnection->send(str(new Response(500, [], $e->getMessage()))); $httpConnection->send(Message::toString(new Response(500, [], $e->getMessage())));
} }
} }
} }

View File

@@ -5,8 +5,8 @@ namespace App\Client\Http\Controllers;
use App\Client\Http\HttpClient; use App\Client\Http\HttpClient;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Logger\RequestLogger; use App\Logger\RequestLogger;
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use function GuzzleHttp\Psr7\str;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
@@ -29,13 +29,15 @@ class ReplayLogController extends Controller
$loggedRequest = $this->requestLogger->findLoggedRequest($request->get('log')); $loggedRequest = $this->requestLogger->findLoggedRequest($request->get('log'));
if (is_null($loggedRequest)) { if (is_null($loggedRequest)) {
$httpConnection->send(str(new Response(404))); $httpConnection->send(Message::toString(new Response(404)));
return; return;
} }
$this->httpClient->performRequest($loggedRequest->getRequestData()); $loggedRequest->refreshId();
$httpConnection->send(str(new Response(200))); $this->httpClient->performRequest($loggedRequest->getRequest()->toString());
$httpConnection->send(Message::toString(new Response(200)));
} }
} }

View File

@@ -2,11 +2,12 @@
namespace App\Client\Http; namespace App\Client\Http;
use App\Client\Configuration;
use App\Client\Http\Modifiers\CheckBasicAuthentication; use App\Client\Http\Modifiers\CheckBasicAuthentication;
use App\Logger\RequestLogger; use App\Logger\RequestLogger;
use Clue\React\Buzz\Browser; use Clue\React\Buzz\Browser;
use GuzzleHttp\Psr7\Message;
use function GuzzleHttp\Psr7\parse_request; use function GuzzleHttp\Psr7\parse_request;
use function GuzzleHttp\Psr7\str;
use Laminas\Http\Request; use Laminas\Http\Request;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@@ -26,19 +27,26 @@ class HttpClient
/** @var Request */ /** @var Request */
protected $request; protected $request;
protected $connectionData;
/** @var array */ /** @var array */
protected $modifiers = [ protected $modifiers = [
CheckBasicAuthentication::class, CheckBasicAuthentication::class,
]; ];
/** @var Configuration */
protected $configuration;
public function __construct(LoopInterface $loop, RequestLogger $logger) public function __construct(LoopInterface $loop, RequestLogger $logger, Configuration $configuration)
{ {
$this->loop = $loop; $this->loop = $loop;
$this->logger = $logger; $this->logger = $logger;
$this->configuration = $configuration;
} }
public function performRequest(string $requestData, WebSocket $proxyConnection = null, string $requestId = null) public function performRequest(string $requestData, WebSocket $proxyConnection = null, $connectionData = null)
{ {
$this->connectionData = $connectionData;
$this->request = $this->parseRequest($requestData); $this->request = $this->parseRequest($requestData);
$this->logger->logRequest($requestData, $this->request); $this->logger->logRequest($requestData, $this->request);
@@ -66,7 +74,7 @@ class HttpClient
protected function createConnector(): Connector protected function createConnector(): Connector
{ {
return new Connector($this->loop, [ return new Connector($this->loop, [
'dns' => '127.0.0.1', 'dns' => config('expose.dns', '127.0.0.1'),
'tls' => [ 'tls' => [
'verify_peer' => false, 'verify_peer' => false,
'verify_peer_name' => false, 'verify_peer_name' => false,
@@ -77,15 +85,19 @@ class HttpClient
protected function sendRequestToApplication(RequestInterface $request, $proxyConnection = null) protected function sendRequestToApplication(RequestInterface $request, $proxyConnection = null)
{ {
(new Browser($this->loop, $this->createConnector())) (new Browser($this->loop, $this->createConnector()))
->withOptions([ ->withFollowRedirects(false)
'followRedirects' => false, ->withRejectErrorResponse(false)
'obeySuccessCode' => false, ->requestStreaming(
'streaming' => true, $request->getMethod(),
]) $request->getUri(),
->send($request) $request->getHeaders(),
$request->getBody()
)
->then(function (ResponseInterface $response) use ($proxyConnection) { ->then(function (ResponseInterface $response) use ($proxyConnection) {
if (! isset($response->buffer)) { if (! isset($response->buffer)) {
$response->buffer = str($response); $response = $this->rewriteResponseHeaders($response);
$response->buffer = Message::toString($response);
} }
$this->sendChunkToServer($response->buffer, $proxyConnection); $this->sendChunkToServer($response->buffer, $proxyConnection);
@@ -93,7 +105,7 @@ class HttpClient
/* @var $body \React\Stream\ReadableStreamInterface */ /* @var $body \React\Stream\ReadableStreamInterface */
$body = $response->getBody(); $body = $response->getBody();
$this->logResponse(str($response)); $this->logResponse(Message::toString($response));
$body->on('data', function ($chunk) use ($proxyConnection, $response) { $body->on('data', function ($chunk) use ($proxyConnection, $response) {
$response->buffer .= $chunk; $response->buffer .= $chunk;
@@ -126,4 +138,25 @@ class HttpClient
{ {
return Request::fromString($data); return Request::fromString($data);
} }
protected function rewriteResponseHeaders(ResponseInterface $response)
{
if (! $response->hasHeader('Location')) {
return $response;
}
$location = $response->getHeaderLine('Location');
if (! strstr($location, $this->connectionData->host)) {
return $response;
}
$location = str_replace(
$this->connectionData->host,
$this->configuration->getUrl($this->connectionData->subdomain),
$location
);
return $response->withHeader('Location', $location);
}
} }

View File

@@ -3,8 +3,8 @@
namespace App\Client\Http\Modifiers; namespace App\Client\Http\Modifiers;
use App\Client\Configuration; use App\Client\Configuration;
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use function GuzzleHttp\Psr7\str;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Ratchet\Client\WebSocket; use Ratchet\Client\WebSocket;
@@ -29,7 +29,7 @@ class CheckBasicAuthentication
if (is_null($username)) { if (is_null($username)) {
$proxyConnection->send( $proxyConnection->send(
str(new Response(401, [ Message::toString(new Response(401, [
'WWW-Authenticate' => 'Basic realm=Expose', 'WWW-Authenticate' => 'Basic realm=Expose',
], 'Unauthorized')) ], 'Unauthorized'))
); );
@@ -89,7 +89,7 @@ class CheckBasicAuthentication
protected function getCredentials() protected function getCredentials()
{ {
try { try {
$credentials = explode(':', $this->configuration->auth()); $credentials = explode(':', $this->configuration->basicAuth());
return [ return [
$credentials[0] => $credentials[1], $credentials[0] => $credentials[1],

View File

@@ -5,7 +5,9 @@ namespace App\Client;
use App\Client\Http\HttpClient; use App\Client\Http\HttpClient;
use function Ratchet\Client\connect; use function Ratchet\Client\connect;
use Ratchet\Client\WebSocket; use Ratchet\Client\WebSocket;
use Ratchet\RFC6455\Messaging\Frame;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
use React\Socket\Connector;
class ProxyManager class ProxyManager
{ {
@@ -30,7 +32,7 @@ class ProxyManager
], $this->loop) ], $this->loop)
->then(function (WebSocket $proxyConnection) use ($clientId, $connectionData) { ->then(function (WebSocket $proxyConnection) use ($clientId, $connectionData) {
$proxyConnection->on('message', function ($message) use ($proxyConnection, $connectionData) { $proxyConnection->on('message', function ($message) use ($proxyConnection, $connectionData) {
$this->performRequest($proxyConnection, $connectionData->request_id, (string) $message); $this->performRequest($proxyConnection, (string) $message, $connectionData);
}); });
$proxyConnection->send(json_encode([ $proxyConnection->send(json_encode([
@@ -43,8 +45,39 @@ class ProxyManager
}); });
} }
protected function performRequest(WebSocket $proxyConnection, $requestId, string $requestData) public function createTcpProxy(string $clientId, $connectionData)
{ {
app(HttpClient::class)->performRequest((string) $requestData, $proxyConnection, $requestId); $protocol = $this->configuration->port() === 443 ? 'wss' : 'ws';
connect($protocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control", [], [
'X-Expose-Control' => 'enabled',
], $this->loop)
->then(function (WebSocket $proxyConnection) use ($clientId, $connectionData) {
$connector = new Connector($this->loop);
$connector->connect('127.0.0.1:'.$connectionData->port)->then(function ($connection) use ($proxyConnection) {
$connection->on('data', function ($data) use ($proxyConnection) {
$binaryMsg = new Frame($data, true, Frame::OP_BINARY);
$proxyConnection->send($binaryMsg);
});
$proxyConnection->on('message', function ($message) use ($connection) {
$connection->write($message);
});
});
$proxyConnection->send(json_encode([
'event' => 'registerTcpProxy',
'data' => [
'tcp_request_id' => $connectionData->tcp_request_id ?? null,
'client_id' => $clientId,
],
]));
});
}
protected function performRequest(WebSocket $proxyConnection, string $requestData, $connectionData)
{
app(HttpClient::class)->performRequest((string) $requestData, $proxyConnection, $connectionData);
} }
} }

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Client\Support;
use PhpParser\Node;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Name;
use PhpParser\NodeVisitorAbstract;
class ClearDomainNodeVisitor extends NodeVisitorAbstract
{
public function enterNode(Node $node)
{
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_domain') {
$node->value = new ConstFetch(
new Name('null')
);
return $node;
}
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace App\Client\Support;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\Terminal;
/**
* @author Pierre du Plessis <pdples@gmail.com>
* @author Gabriel Ostrolucký <gabriel.ostrolucky@gmail.com>
*/
class ConsoleSectionOutput extends StreamOutput
{
private $content = [];
private $lines = 0;
private $sections;
private $terminal;
/**
* @param resource $stream
* @param \Symfony\Component\Console\Output\ConsoleSectionOutput[] $sections
*/
public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter)
{
parent::__construct($stream, $verbosity, $decorated, $formatter);
array_unshift($sections, $this);
$this->sections = &$sections;
$this->terminal = new Terminal();
}
/**
* Clears previous output for this section.
*
* @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared
*/
public function clear(int $lines = null)
{
if (empty($this->content) || ! $this->isDecorated()) {
return;
}
if ($lines) {
array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content
} else {
$lines = $this->lines;
$this->content = [];
}
$this->lines -= $lines;
parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false);
}
/**
* Overwrites the previous output with a new message.
*
* @param array|string $message
*/
public function overwrite($message)
{
$this->clear();
$this->writeln($message);
}
public function getContent(): string
{
return implode('', $this->content);
}
/**
* @internal
*/
public function addContent(string $input)
{
foreach (explode(\PHP_EOL, $input) as $lineContent) {
$this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1;
$this->content[] = $lineContent;
$this->content[] = \PHP_EOL;
}
}
/**
* {@inheritdoc}
*/
protected function doWrite(string $message, bool $newline)
{
if (! $this->isDecorated()) {
parent::doWrite($message, $newline);
return;
}
$erasedContent = $this->popStreamContentUntilCurrentSection();
$this->addContent($message);
parent::doWrite($message, true);
parent::doWrite($erasedContent, false);
}
/**
* At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits
* current section. Then it erases content it crawled through. Optionally, it erases part of current section too.
*/
private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string
{
$numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection;
$erasedContent = [];
foreach ($this->sections as $section) {
if ($section === $this) {
break;
}
$numberOfLinesToClear += $section->lines;
$erasedContent[] = $section->getContent();
}
if ($numberOfLinesToClear > 0) {
// move cursor up n lines
parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false);
// erase to end of screen
parent::doWrite("\x1b[0J", false);
}
return implode('', array_reverse($erasedContent));
}
private function getDisplayLength(string $text): int
{
$cleanedText = Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text));
$cleanedText = preg_replace('/]8;;(.*)]8;;/m', '', $cleanedText);
return Helper::width($cleanedText);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Client\Support;
use PhpParser\Node;
use PhpParser\Node\Scalar\String_;
use PhpParser\NodeVisitorAbstract;
class DefaultDomainNodeVisitor extends NodeVisitorAbstract
{
/** @var string */
protected $domain;
public function __construct(string $domain)
{
$this->domain = $domain;
}
public function leaveNode(Node $node)
{
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_domain') {
$node->value = new String_($this->domain);
return $node;
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Client\Support;
use PhpParser\Node;
use PhpParser\Node\Scalar\String_;
use PhpParser\NodeVisitorAbstract;
class DefaultServerNodeVisitor extends NodeVisitorAbstract
{
/** @var string */
protected $server;
public function __construct(string $server)
{
$this->server = $server;
}
public function enterNode(Node $node)
{
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_server') {
$node->value = new String_($this->server);
return $node;
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Client\Support;
use PhpParser\Node;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Name;
use PhpParser\NodeVisitorAbstract;
class InsertDefaultDomainNodeVisitor extends NodeVisitorAbstract
{
public function leaveNode(Node $node)
{
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'auth_token') {
$defaultDomainNode = new Node\Expr\ArrayItem(
new ConstFetch(
new Name('null')
),
new Node\Scalar\String_('default_domain')
);
return [
$node,
$defaultDomainNode,
];
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Client\Support;
use PhpParser\Node;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Name;
use PhpParser\NodeVisitorAbstract;
class InsertDefaultServerNodeVisitor extends NodeVisitorAbstract
{
public function leaveNode(Node $node)
{
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'auth_token') {
$defaultServerNode = new Node\Expr\ArrayItem(
new ConstFetch(
new Name('null')
),
new Node\Scalar\String_('default_server')
);
return [
$node,
$defaultServerNode,
];
}
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Client\Support; namespace App\Client\Support;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\Scalar\String_;
use PhpParser\NodeVisitorAbstract; use PhpParser\NodeVisitorAbstract;
class TokenNodeVisitor extends NodeVisitorAbstract class TokenNodeVisitor extends NodeVisitorAbstract
@@ -18,7 +19,7 @@ class TokenNodeVisitor extends NodeVisitorAbstract
public function enterNode(Node $node) public function enterNode(Node $node)
{ {
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'auth_token') { if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'auth_token') {
$node->value->value = $this->token; $node->value = new String_($this->token);
return $node; return $node;
} }

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Commands;
use App\Client\Support\ClearDomainNodeVisitor;
use App\Client\Support\InsertDefaultDomainNodeVisitor;
use Illuminate\Console\Command;
use PhpParser\Lexer\Emulative;
use PhpParser\Node;
use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\CloningVisitor;
use PhpParser\Parser\Php7;
use PhpParser\PrettyPrinter\Standard;
class ClearDefaultDomainCommand extends Command
{
protected $signature = 'default-domain:clear';
protected $description = 'Clear the default domain to use with Expose.';
public function handle()
{
$this->info('Clearing the default Expose domain.');
$configFile = implode(DIRECTORY_SEPARATOR, [
$_SERVER['HOME'] ?? $_SERVER['USERPROFILE'],
'.expose',
'config.php',
]);
if (! file_exists($configFile)) {
@mkdir(dirname($configFile), 0777, true);
$updatedConfigFile = $this->modifyConfigurationFile(base_path('config/expose.php'));
} else {
$updatedConfigFile = $this->modifyConfigurationFile($configFile);
}
file_put_contents($configFile, $updatedConfigFile);
}
protected function modifyConfigurationFile(string $configFile)
{
$lexer = new Emulative([
'usedAttributes' => [
'comments',
'startLine', 'endLine',
'startTokenPos', 'endTokenPos',
],
]);
$parser = new Php7($lexer);
$oldStmts = $parser->parse(file_get_contents($configFile));
$oldTokens = $lexer->getTokens();
$nodeTraverser = new NodeTraverser;
$nodeTraverser->addVisitor(new CloningVisitor());
$newStmts = $nodeTraverser->traverse($oldStmts);
$nodeFinder = new NodeFinder;
$defaultDomainNode = $nodeFinder->findFirst($newStmts, function (Node $node) {
return $node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_domain';
});
if (is_null($defaultDomainNode)) {
$nodeTraverser = new NodeTraverser;
$nodeTraverser->addVisitor(new InsertDefaultDomainNodeVisitor());
$newStmts = $nodeTraverser->traverse($newStmts);
}
$nodeTraverser = new NodeTraverser;
$nodeTraverser->addVisitor(new ClearDomainNodeVisitor());
$newStmts = $nodeTraverser->traverse($newStmts);
$prettyPrinter = new Standard();
return $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens);
}
}

View File

@@ -3,20 +3,35 @@
namespace App\Commands; namespace App\Commands;
use App\Server\Factory; use App\Server\Factory;
use InvalidArgumentException;
use LaravelZero\Framework\Commands\Command; use LaravelZero\Framework\Commands\Command;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
class ServeCommand extends Command class ServeCommand extends Command
{ {
protected $signature = 'serve {hostname=localhost} {host=0.0.0.0} {--validateAuthTokens} {--port=8080}'; protected $signature = 'serve {hostname=localhost} {host=0.0.0.0} {--validateAuthTokens} {--port=8080} {--config=}';
protected $description = 'Start the expose server'; protected $description = 'Start the expose server';
protected function loadConfiguration(string $configFile)
{
$configFile = realpath($configFile);
throw_if(! file_exists($configFile), new InvalidArgumentException("Invalid config file {$configFile}"));
$localConfig = require $configFile;
config()->set('expose', $localConfig);
}
public function handle() public function handle()
{ {
/** @var LoopInterface $loop */ /** @var LoopInterface $loop */
$loop = app(LoopInterface::class); $loop = app(LoopInterface::class);
if ($this->option('config')) {
$this->loadConfiguration($this->option('config'));
}
$loop->futureTick(function () { $loop->futureTick(function () {
$this->info('Expose server running on port '.$this->option('port').'.'); $this->info('Expose server running on port '.$this->option('port').'.');
}); });

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Commands;
use App\Client\Exceptions\InvalidServerProvided;
use App\Logger\CliRequestLogger;
use Illuminate\Console\Parser;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use LaravelZero\Framework\Commands\Command;
use Symfony\Component\Console\Output\ConsoleOutput;
abstract class ServerAwareCommand extends Command
{
const DEFAULT_HOSTNAME = 'bitinflow.dev';
const DEFAULT_PORT = 443;
const DEFAULT_SERVER_ENDPOINT = 'https://expose.dev/api/servers';
public function __construct()
{
parent::__construct();
$inheritedSignature = '{--server=} {--server-host=} {--server-port=}';
$this->getDefinition()->addOptions(Parser::parse($inheritedSignature)[2]);
$this->configureConnectionLogger();
}
protected function configureConnectionLogger()
{
app()->singleton(CliRequestLogger::class, function () {
return new CliRequestLogger(new ConsoleOutput());
});
return $this;
}
protected function getServerHost()
{
if ($this->option('server-host')) {
return $this->option('server-host');
}
/**
* Try to find the server in the servers array.
* If no array exists at all (when upgrading from v1),
* always return bitinflow.dev.
*/
if (config('expose.servers') === null) {
return static::DEFAULT_HOSTNAME;
}
$server = $this->option('server') ?? config('expose.default_server');
$host = config('expose.servers.'.$server.'.host');
if (! is_null($host)) {
return $host;
}
return $this->lookupRemoteServerHost($server);
}
protected function getServerPort()
{
if ($this->option('server-port')) {
return $this->option('server-port');
}
/**
* Try to find the server in the servers array.
* If no array exists at all (when upgrading from v1),
* always return bitinflow.dev.
*/
if (config('expose.servers') === null) {
return static::DEFAULT_PORT;
}
$server = $this->option('server') ?? config('expose.default_server');
$host = config('expose.servers.'.$server.'.port');
if (! is_null($host)) {
return $host;
}
return $this->lookupRemoteServerPort($server);
}
protected function lookupRemoteServers()
{
try {
return Http::withOptions([
'verify' => false,
])->get(config('expose.server_endpoint', static::DEFAULT_SERVER_ENDPOINT))->json();
} catch (\Throwable $e) {
return [];
}
}
protected function lookupRemoteServerHost($server)
{
$servers = $this->lookupRemoteServers();
$host = Arr::get($servers, $server.'.host');
throw_if(is_null($host), new InvalidServerProvided($server));
return $host;
}
protected function lookupRemoteServerPort($server)
{
$servers = $this->lookupRemoteServers();
$port = Arr::get($servers, $server.'.port');
throw_if(is_null($port), new InvalidServerProvided($server));
return $port;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Commands;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use LaravelZero\Framework\Commands\Command;
class ServerListCommand extends Command
{
const DEFAULT_SERVER_ENDPOINT = 'https://expose.dev/api/servers';
protected $signature = 'servers';
protected $description = 'Set or retrieve the default server to use with Expose.';
public function handle()
{
$servers = collect($this->lookupRemoteServers())->map(function ($server) {
return [
'key' => $server['key'],
'region' => $server['region'],
'plan' => Str::ucfirst($server['plan']),
];
});
$this->info('You can connect to a specific server with the --server=key option or set this server as default with the default-server command.');
$this->info('');
$this->table(['Key', 'Region', 'Type'], $servers);
}
protected function lookupRemoteServers()
{
try {
return Http::withOptions([
'verify' => false,
])->get(config('expose.server_endpoint', static::DEFAULT_SERVER_ENDPOINT))->json();
} catch (\Throwable $e) {
return [];
}
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Commands;
use App\Client\Support\DefaultDomainNodeVisitor;
use App\Client\Support\DefaultServerNodeVisitor;
use App\Client\Support\InsertDefaultDomainNodeVisitor;
use Illuminate\Console\Command;
use PhpParser\Lexer\Emulative;
use PhpParser\Node;
use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\CloningVisitor;
use PhpParser\Parser\Php7;
use PhpParser\PrettyPrinter\Standard;
class SetDefaultDomainCommand extends Command
{
protected $signature = 'default-domain {domain?} {--server=}';
protected $description = 'Set or retrieve the default domain to use with Expose.';
public function handle()
{
$domain = $this->argument('domain');
$server = $this->option('server');
if (! is_null($domain)) {
$this->info('Setting the Expose default domain to "'.$domain.'"');
$configFile = implode(DIRECTORY_SEPARATOR, [
$_SERVER['HOME'] ?? $_SERVER['USERPROFILE'],
'.expose',
'config.php',
]);
if (! file_exists($configFile)) {
@mkdir(dirname($configFile), 0777, true);
$updatedConfigFile = $this->modifyConfigurationFile(base_path('config/expose.php'), $domain, $server);
} else {
$updatedConfigFile = $this->modifyConfigurationFile($configFile, $domain, $server);
}
file_put_contents($configFile, $updatedConfigFile);
return;
}
if (is_null($domain = config('expose.default_domain'))) {
$this->info('There is no default domain specified.');
} else {
$this->info('Current default domain: '.$domain);
}
}
protected function modifyConfigurationFile(string $configFile, string $domain, ?string $server)
{
$lexer = new Emulative([
'usedAttributes' => [
'comments',
'startLine', 'endLine',
'startTokenPos', 'endTokenPos',
],
]);
$parser = new Php7($lexer);
$oldStmts = $parser->parse(file_get_contents($configFile));
$oldTokens = $lexer->getTokens();
$nodeTraverser = new NodeTraverser;
$nodeTraverser->addVisitor(new CloningVisitor());
$newStmts = $nodeTraverser->traverse($oldStmts);
$nodeFinder = new NodeFinder;
$defaultDomainNode = $nodeFinder->findFirst($newStmts, function (Node $node) {
return $node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_domain';
});
if (is_null($defaultDomainNode)) {
$nodeTraverser = new NodeTraverser;
$nodeTraverser->addVisitor(new InsertDefaultDomainNodeVisitor());
$newStmts = $nodeTraverser->traverse($newStmts);
}
$nodeTraverser = new NodeTraverser;
$nodeTraverser->addVisitor(new DefaultDomainNodeVisitor($domain));
if (! is_null($server)) {
$nodeTraverser->addVisitor(new DefaultServerNodeVisitor($server));
}
$newStmts = $nodeTraverser->traverse($newStmts);
$prettyPrinter = new Standard();
return $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Commands;
use App\Client\Support\DefaultServerNodeVisitor;
use App\Client\Support\InsertDefaultServerNodeVisitor;
use Illuminate\Console\Command;
use PhpParser\Lexer\Emulative;
use PhpParser\Node;
use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\CloningVisitor;
use PhpParser\Parser\Php7;
use PhpParser\PrettyPrinter\Standard;
class SetDefaultServerCommand extends Command
{
protected $signature = 'default-server {server?}';
protected $description = 'Set or retrieve the default server to use with Expose.';
public function handle()
{
$server = $this->argument('server');
if (! is_null($server)) {
$this->info('Setting the Expose default server to "'.$server.'"');
$configFile = implode(DIRECTORY_SEPARATOR, [
$_SERVER['HOME'] ?? $_SERVER['USERPROFILE'],
'.expose',
'config.php',
]);
if (! file_exists($configFile)) {
@mkdir(dirname($configFile), 0777, true);
$updatedConfigFile = $this->modifyConfigurationFile(base_path('config/expose.php'), $server);
} else {
$updatedConfigFile = $this->modifyConfigurationFile($configFile, $server);
}
file_put_contents($configFile, $updatedConfigFile);
return;
}
if (is_null($server = config('expose.default_server'))) {
$this->info('There is no default server specified.');
} else {
$this->info('Current default server: '.$server);
}
}
protected function modifyConfigurationFile(string $configFile, string $server)
{
$lexer = new Emulative([
'usedAttributes' => [
'comments',
'startLine', 'endLine',
'startTokenPos', 'endTokenPos',
],
]);
$parser = new Php7($lexer);
$oldStmts = $parser->parse(file_get_contents($configFile));
$oldTokens = $lexer->getTokens();
$nodeTraverser = new NodeTraverser;
$nodeTraverser->addVisitor(new CloningVisitor());
$newStmts = $nodeTraverser->traverse($oldStmts);
$nodeFinder = new NodeFinder;
$defaultServerNode = $nodeFinder->findFirst($newStmts, function (Node $node) {
return $node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_server';
});
if (is_null($defaultServerNode)) {
$nodeTraverser = new NodeTraverser;
$nodeTraverser->addVisitor(new InsertDefaultServerNodeVisitor());
$newStmts = $nodeTraverser->traverse($newStmts);
}
$nodeTraverser = new NodeTraverser;
$nodeTraverser->addVisitor(new DefaultServerNodeVisitor($server));
$newStmts = $nodeTraverser->traverse($newStmts);
$prettyPrinter = new Standard();
return $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens);
}
}

View File

@@ -3,37 +3,63 @@
namespace App\Commands; namespace App\Commands;
use App\Client\Factory; use App\Client\Factory;
use App\Logger\CliRequestLogger; use Illuminate\Support\Str;
use LaravelZero\Framework\Commands\Command;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface;
class ShareCommand extends Command class ShareCommand extends ServerAwareCommand
{ {
protected $signature = 'share {host} {--subdomain=} {--auth=}'; protected $signature = 'share {host} {--subdomain=} {--auth=} {--basicAuth=} {--dns=} {--domain=}';
protected $description = 'Share a local url with a remote expose server'; protected $description = 'Share a local url with a remote expose server';
protected function configureConnectionLogger()
{
app()->bind(CliRequestLogger::class, function () {
return new CliRequestLogger(new ConsoleOutput());
});
return $this;
}
public function handle() public function handle()
{ {
$this->configureConnectionLogger(); $auth = $this->option('auth') ?? config('expose.auth_token', '');
$this->info('Using auth token: '.$auth, OutputInterface::VERBOSITY_DEBUG);
if (strstr($this->argument('host'), 'host.docker.internal')) {
config(['expose.dns' => true]);
}
if ($this->option('dns') !== null) {
config(['expose.dns' => empty($this->option('dns')) ? true : $this->option('dns')]);
}
$domain = config('expose.default_domain');
if (! is_null($this->option('server'))) {
$domain = null;
}
if (! is_null($this->option('domain'))) {
$domain = $this->option('domain');
}
if (! is_null($this->option('subdomain'))) {
$subdomains = explode(',', $this->option('subdomain'));
$this->info('Trying to use custom domain: '.$subdomains[0].PHP_EOL, OutputInterface::VERBOSITY_VERBOSE);
} else {
$host = Str::beforeLast($this->argument('host'), '.');
$host = str_replace('https://', '', $host);
$host = str_replace('http://', '', $host);
$host = Str::beforeLast($host, ':');
$subdomains = [Str::slug($host)];
$this->info('Trying to use custom domain: '.$subdomains[0].PHP_EOL, OutputInterface::VERBOSITY_VERBOSE);
}
(new Factory()) (new Factory())
->setLoop(app(LoopInterface::class)) ->setLoop(app(LoopInterface::class))
->setHost(config('expose.host', 'localhost')) ->setHost($this->getServerHost())
->setPort(config('expose.port', 8080)) ->setPort($this->getServerPort())
->setAuth($this->option('auth')) ->setAuth($auth)
->setBasicAuth($this->option('basicAuth'))
->createClient() ->createClient()
->share($this->argument('host'), explode(',', $this->option('subdomain'))) ->share(
$this->argument('host'),
$subdomains,
$domain
)
->createHttpServer() ->createHttpServer()
->run(); ->run();
} }

View File

@@ -4,15 +4,17 @@ namespace App\Commands;
class ShareCurrentWorkingDirectoryCommand extends ShareCommand class ShareCurrentWorkingDirectoryCommand extends ShareCommand
{ {
protected $signature = 'share-cwd {host?} {--subdomain=} {--auth=}'; protected $signature = 'share-cwd {host?} {--subdomain=} {--auth=} {--basicAuth=} {--dns=} {--domain=}';
public function handle() public function handle()
{ {
$this->input->setArgument('host', basename(getcwd()).'.'.$this->detectTld()); $folderName = $this->detectName();
$host = $this->prepareSharedHost($folderName.'.'.$this->detectTld());
if (! $this->hasOption('subdomain')) { $this->input->setArgument('host', $host);
$subdomain = str_replace('.', '_', basename(getcwd()));
$this->input->setOption('subdomain', $subdomain); if (! $this->option('subdomain')) {
$this->input->setOption('subdomain', str_replace('.', '-', $folderName));
} }
parent::handle(); parent::handle();
@@ -20,7 +22,7 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
protected function detectTld(): string protected function detectTld(): string
{ {
$valetConfigFile = $_SERVER['HOME'] ?? $_SERVER['USERPROFILE'].DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'config.json'; $valetConfigFile = ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']).DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'config.json';
if (file_exists($valetConfigFile)) { if (file_exists($valetConfigFile)) {
$valetConfig = json_decode(file_get_contents($valetConfigFile)); $valetConfig = json_decode(file_get_contents($valetConfigFile));
@@ -30,4 +32,46 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
return config('expose.default_tld', 'test'); return config('expose.default_tld', 'test');
} }
protected function detectName(): string
{
$projectPath = getcwd();
$valetSitesPath = ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']).DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'Sites';
if (is_dir($valetSitesPath)) {
$site = collect(scandir($valetSitesPath))
->skip(2)
->map(function ($site) use ($valetSitesPath) {
return $valetSitesPath.DIRECTORY_SEPARATOR.$site;
})->mapWithKeys(function ($site) {
return [$site => readlink($site)];
})->filter(function ($sourcePath) use ($projectPath) {
return $sourcePath === $projectPath;
})
->keys()
->first();
if ($site) {
$projectPath = $site;
}
}
return basename($projectPath);
}
protected function detectProtocol($host): string
{
$certificateFile = ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']).DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'Certificates'.DIRECTORY_SEPARATOR.$host.'.crt';
if (file_exists($certificateFile)) {
return 'https://';
}
return config('expose.default_https', false) ? 'https://' : 'http://';
}
protected function prepareSharedHost($host): string
{
return $this->detectProtocol($host).$host;
}
} }

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Commands;
use App\Client\Factory;
use React\EventLoop\LoopInterface;
class SharePortCommand extends ServerAwareCommand
{
protected $signature = 'share-port {port} {--auth=}';
protected $description = 'Share a local port with a remote expose server';
public function handle()
{
$auth = $this->option('auth') ?? config('expose.auth_token', '');
(new Factory())
->setLoop(app(LoopInterface::class))
->setHost($this->getServerHost())
->setPort($this->getServerPort())
->setAuth($auth)
->createClient()
->sharePort($this->argument('port'))
->createHttpServer()
->run();
}
}

View File

@@ -14,7 +14,7 @@ class StoreAuthenticationTokenCommand extends Command
{ {
protected $signature = 'token {token?}'; protected $signature = 'token {token?}';
protected $description = 'Set or retrieve the authentication token to use with expose.'; protected $description = 'Set or retrieve the authentication token to use with Expose.';
public function handle() public function handle()
{ {

View File

@@ -8,7 +8,9 @@ use Ratchet\ConnectionInterface;
interface ConnectionManager interface ConnectionManager
{ {
public function storeConnection(string $host, ?string $subdomain, ConnectionInterface $connection): ControlConnection; public function storeConnection(string $host, ?string $subdomain, ?string $serverHost, ConnectionInterface $connection): ControlConnection;
public function storeTcpConnection(int $port, ConnectionInterface $connection): ControlConnection;
public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength); public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength);
@@ -18,9 +20,17 @@ interface ConnectionManager
public function removeControlConnection($connection); public function removeControlConnection($connection);
public function findControlConnectionForSubdomain($subdomain): ?ControlConnection; public function findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost): ?ControlConnection;
public function findControlConnectionForClientId(string $clientId): ?ControlConnection; public function findControlConnectionForClientId(string $clientId): ?ControlConnection;
public function getConnections(): array; public function getConnections(): array;
public function getConnectionsForAuthToken(string $authToken): array;
public function getTcpConnectionsForAuthToken(string $authToken): array;
public function findControlConnectionsForIp(string $ip): array;
public function findControlConnectionsForAuthToken(string $token): array;
} }

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Contracts;
use React\Promise\PromiseInterface;
interface DomainRepository
{
public function getDomains(): PromiseInterface;
public function getDomainById($id): PromiseInterface;
public function getDomainByName(string $name): PromiseInterface;
public function getDomainsByUserId($id): PromiseInterface;
public function getDomainsByUserIdAndName($id, $name): PromiseInterface;
public function deleteDomainForUserId($userId, $domainId): PromiseInterface;
public function storeDomain(array $data): PromiseInterface;
public function updateDomain($id, array $data): PromiseInterface;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Contracts;
use React\Promise\PromiseInterface;
interface LoggerRepository
{
public function logSubdomain($authToken, $subdomain);
public function getLogs(): PromiseInterface;
public function getLogsBySubdomain($subdomain): PromiseInterface;
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Contracts;
interface StatisticsCollector
{
public function siteShared($authToken = null);
public function portShared($authToken = null);
public function incomingRequest();
public function flush();
public function save();
public function shouldCollectStatistics(): bool;
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Contracts;
use React\Promise\PromiseInterface;
interface StatisticsRepository
{
public function getStatistics($from, $until): PromiseInterface;
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Contracts;
use React\Promise\PromiseInterface;
interface SubdomainRepository
{
public function getSubdomains(): PromiseInterface;
public function getSubdomainById($id): PromiseInterface;
public function getSubdomainByName(string $name): PromiseInterface;
public function getSubdomainByNameAndDomain(string $name, string $domain): PromiseInterface;
public function getSubdomainsByNameAndDomain(string $name, string $domain): PromiseInterface;
public function getSubdomainsByUserId($id): PromiseInterface;
public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface;
public function deleteSubdomainForUserId($userId, $subdomainId): PromiseInterface;
public function storeSubdomain(array $data): PromiseInterface;
}

View File

@@ -10,9 +10,15 @@ interface UserRepository
public function getUserById($id): PromiseInterface; public function getUserById($id): PromiseInterface;
public function paginateUsers(string $searchQuery, int $perPage, int $currentPage): PromiseInterface;
public function getUserByToken(string $authToken): PromiseInterface; public function getUserByToken(string $authToken): PromiseInterface;
public function storeUser(array $data): PromiseInterface; public function storeUser(array $data): PromiseInterface;
public function deleteUser($id): PromiseInterface; public function deleteUser($id): PromiseInterface;
public function getUsersByTokens(array $authTokens): PromiseInterface;
public function updateLastSharedAt($id): PromiseInterface;
} }

View File

@@ -9,7 +9,7 @@ use Twig\Loader\ArrayLoader;
trait LoadsViews trait LoadsViews
{ {
protected function getView(ConnectionInterface $connection, string $view, array $data = []) protected function getView(?ConnectionInterface $connection, string $view, array $data = [])
{ {
$templatePath = implode(DIRECTORY_SEPARATOR, explode('.', $view)); $templatePath = implode(DIRECTORY_SEPARATOR, explode('.', $view));
@@ -23,7 +23,10 @@ trait LoadsViews
$data = array_merge($data, [ $data = array_merge($data, [
'request' => $connection->laravelRequest ?? null, 'request' => $connection->laravelRequest ?? null,
]); ]);
try {
return stream_for($twig->render('template', $data)); return stream_for($twig->render('template', $data));
} catch (\Throwable $e) {
var_dump($e->getMessage());
}
} }
} }

View File

@@ -2,29 +2,60 @@
namespace App\Logger; namespace App\Logger;
use App\Client\Support\ConsoleSectionOutput;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Terminal;
class CliRequestLogger extends Logger class CliRequestLogger extends Logger
{ {
/** @var Table */
protected $table;
/** @var Collection */ /** @var Collection */
protected $requests; protected $requests;
/** @var \Symfony\Component\Console\Output\ConsoleSectionOutput */
protected $section; protected $section;
protected $verbColors = [
'GET' => 'blue',
'HEAD' => '#6C7280',
'OPTIONS' => '#6C7280',
'POST' => 'yellow',
'PUT' => 'yellow',
'PATCH' => 'yellow',
'DELETE' => 'red',
];
protected $consoleSectionOutputs = [];
/**
* The current terminal width.
*
* @var int|null
*/
protected $terminalWidth;
/**
* Computes the terminal width.
*
* @return int
*/
protected function getTerminalWidth()
{
if ($this->terminalWidth == null) {
$this->terminalWidth = (new Terminal)->getWidth();
$this->terminalWidth = $this->terminalWidth >= 30
? $this->terminalWidth
: 30;
}
return $this->terminalWidth;
}
public function __construct(ConsoleOutputInterface $consoleOutput) public function __construct(ConsoleOutputInterface $consoleOutput)
{ {
parent::__construct($consoleOutput); parent::__construct($consoleOutput);
$this->section = $this->output->section(); $this->section = new ConsoleSectionOutput($this->output->getStream(), $this->consoleSectionOutputs, $this->output->getVerbosity(), $this->output->isDecorated(), $this->output->getFormatter());
$this->table = new Table($this->section);
$this->table->setHeaders(['Method', 'URI', 'Response', 'Duration']);
$this->requests = new Collection(); $this->requests = new Collection();
} }
@@ -37,8 +68,28 @@ class CliRequestLogger extends Logger
return $this->output; return $this->output;
} }
protected function getRequestColor(?LoggedRequest $request)
{
$statusCode = optional($request->getResponse())->getStatusCode();
$color = 'white';
if ($statusCode >= 200 && $statusCode < 300) {
$color = 'green';
} elseif ($statusCode >= 300 && $statusCode < 400) {
$color = 'blue';
} elseif ($statusCode >= 400 && $statusCode < 500) {
$color = 'yellow';
} elseif ($statusCode >= 500) {
$color = 'red';
}
return $color;
}
public function logRequest(LoggedRequest $loggedRequest) public function logRequest(LoggedRequest $loggedRequest)
{ {
$dashboardUrl = 'http://127.0.0.1:'.config('expose.dashboard_port');
if ($this->requests->has($loggedRequest->id())) { if ($this->requests->has($loggedRequest->id())) {
$this->requests[$loggedRequest->id()] = $loggedRequest; $this->requests[$loggedRequest->id()] = $loggedRequest;
} else { } else {
@@ -46,17 +97,55 @@ class CliRequestLogger extends Logger
} }
$this->requests = $this->requests->slice(0, config('expose.max_logged_requests', 10)); $this->requests = $this->requests->slice(0, config('expose.max_logged_requests', 10));
$this->section->clear(); $terminalWidth = $this->getTerminalWidth();
$this->table->setRows($this->requests->map(function (LoggedRequest $loggedRequest) { $requests = $this->requests->map(function (LoggedRequest $loggedRequest) {
return [ return [
$loggedRequest->getRequest()->getMethod(), 'method' => $loggedRequest->getRequest()->getMethod(),
$loggedRequest->getRequest()->getUri(), 'url' => $loggedRequest->getRequest()->getUri(),
optional($loggedRequest->getResponse())->getStatusCode().' '.optional($loggedRequest->getResponse())->getReasonPhrase(), 'duration' => $loggedRequest->getDuration(),
$loggedRequest->getDuration().'ms', 'time' => $loggedRequest->getStartTime()->isToday() ? $loggedRequest->getStartTime()->toTimeString() : $loggedRequest->getStartTime()->toDateTimeString(),
'color' => $this->getRequestColor($loggedRequest),
'status' => optional($loggedRequest->getResponse())->getStatusCode(),
]; ];
})->toArray()); });
$this->table->render(); $maxMethod = mb_strlen($requests->max('method'));
$maxDuration = mb_strlen($requests->max('duration'));
$output = $requests->map(function ($loggedRequest) use ($terminalWidth, $maxMethod, $maxDuration) {
$method = $loggedRequest['method'];
$spaces = str_repeat(' ', max($maxMethod + 2 - mb_strlen($method), 0));
$url = $loggedRequest['url'];
$duration = $loggedRequest['duration'];
$time = $loggedRequest['time'];
$durationSpaces = str_repeat(' ', max($maxDuration + 2 - mb_strlen($duration), 0));
$color = $loggedRequest['color'];
$status = $loggedRequest['status'];
$dots = str_repeat('.', max($terminalWidth - strlen($method.$spaces.$url.$time.$durationSpaces.$duration) - 16, 0));
if (empty($dots)) {
$url = substr($url, 0, $terminalWidth - strlen($method.$spaces.$time.$durationSpaces.$duration) - 15 - 3).'...';
} else {
$dots .= ' ';
}
return sprintf(
' <fg=%s;options=bold>%s </> <fg=%s;options=bold>%s%s</> %s<fg=#6C7280> %s%s%s%s ms</>',
$color,
$status,
$this->verbColors[$method] ?? 'default',
$method,
$spaces,
$url,
$dots,
$time,
$durationSpaces,
$duration,
);
});
$this->section->overwrite($output);
} }
} }

View File

@@ -3,9 +3,11 @@
namespace App\Logger; namespace App\Logger;
use Carbon\Carbon; use Carbon\Carbon;
use GuzzleHttp\Psr7\Message;
use function GuzzleHttp\Psr7\parse_request; use function GuzzleHttp\Psr7\parse_request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Laminas\Http\Header\GenericHeader;
use Laminas\Http\Request; use Laminas\Http\Request;
use Laminas\Http\Response; use Laminas\Http\Response;
use Namshi\Cuzzle\Formatter\CurlFormatter; use Namshi\Cuzzle\Formatter\CurlFormatter;
@@ -19,11 +21,8 @@ class LoggedRequest implements \JsonSerializable
/** @var Request */ /** @var Request */
protected $parsedRequest; protected $parsedRequest;
/** @var string */ /** @var LoggedResponse */
protected $rawResponse; protected $response;
/** @var Response */
protected $parsedResponse;
/** @var string */ /** @var string */
protected $id; protected $id;
@@ -51,6 +50,7 @@ class LoggedRequest implements \JsonSerializable
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
#[\ReturnTypeWillChange]
public function jsonSerialize() public function jsonSerialize()
{ {
$data = [ $data = [
@@ -71,22 +71,8 @@ class LoggedRequest implements \JsonSerializable
], ],
]; ];
if ($this->parsedResponse) { if ($this->response) {
$logBody = $this->shouldReturnBody(); $data['response'] = $this->response->toArray();
try {
$body = $logBody ? $this->parsedResponse->getBody() : '';
} catch (\Exception $e) {
$body = '';
}
$data['response'] = [
'raw' => $logBody ? $this->rawResponse : 'SKIPPED BY CONFIG OR BINARY RESPONSE',
'status' => $this->parsedResponse->getStatusCode(),
'headers' => $this->parsedResponse->getHeaders()->toArray(),
'reason' => $this->parsedResponse->getReasonPhrase(),
'body' => $logBody ? $body : 'SKIPPED BY CONFIG OR BINARY RESPONSE',
];
} }
return $data; return $data;
@@ -107,96 +93,6 @@ class LoggedRequest implements \JsonSerializable
return preg_match('~[^\x20-\x7E\t\r\n]~', $string) > 0; return preg_match('~[^\x20-\x7E\t\r\n]~', $string) > 0;
} }
protected function shouldReturnBody(): bool
{
if ($this->skipByStatus()) {
return false;
}
if ($this->skipByContentType()) {
return false;
}
if ($this->skipByExtension()) {
return false;
}
if ($this->skipBySize()) {
return false;
}
$header = $this->parsedResponse->getHeaders()->get('Content-Type');
$contentType = $header ? $header->getMediaType() : '';
$patterns = [
'application/json',
'text/*',
'*javascript*',
];
return Str::is($patterns, $contentType);
}
protected function skipByStatus(): bool
{
if (empty(config()->get('expose.skip_body_log.status'))) {
return false;
}
return Str::is(config()->get('expose.skip_body_log.status'), $this->parsedResponse->getStatusCode());
}
protected function skipByContentType(): bool
{
if (empty(config()->get('expose.skip_body_log.content_type'))) {
return false;
}
$header = $this->parsedResponse->getHeaders()->get('Content-Type');
$contentType = $header ? $header->getMediaType() : '';
return Str::is(config()->get('expose.skip_body_log.content_type'), $contentType);
}
protected function skipByExtension(): bool
{
if (empty(config()->get('expose.skip_body_log.extension'))) {
return false;
}
return Str::is(config()->get('expose.skip_body_log.extension'), $this->parsedRequest->getUri()->getPath());
}
protected function skipBySize(): bool
{
$configSize = $this->getConfigSize(config()->get('expose.skip_body_log.size', '1MB'));
$contentLength = $this->parsedResponse->getHeaders()->get('Content-Length');
if (! $contentLength) {
return false;
}
$contentSize = $contentLength->getFieldValue() ?? 0;
return $contentSize > $configSize;
}
protected function getConfigSize(string $size): int
{
$units = ['B', 'KB', 'MB', 'GB'];
$number = substr($size, 0, -2);
$suffix = strtoupper(substr($size, -2));
// B or no suffix
if (is_numeric(substr($suffix, 0, 1))) {
return preg_replace('/[^\d]/', '', $size);
}
// if we have an error in the input, default to GB
$exponent = array_flip($units)[$suffix] ?? 5;
return $number * (1024 ** $exponent);
}
public function getRequest() public function getRequest()
{ {
return $this->parsedRequest; return $this->parsedRequest;
@@ -204,9 +100,7 @@ class LoggedRequest implements \JsonSerializable
public function setResponse(string $rawResponse, Response $response) public function setResponse(string $rawResponse, Response $response)
{ {
$this->parsedResponse = $response; $this->response = new LoggedResponse($rawResponse, $response, $this->getRequest());
$this->rawResponse = $rawResponse;
if (is_null($this->stopTime)) { if (is_null($this->stopTime)) {
$this->stopTime = now(); $this->stopTime = now();
@@ -223,9 +117,9 @@ class LoggedRequest implements \JsonSerializable
return $this->rawRequest; return $this->rawRequest;
} }
public function getResponse(): ?Response public function getResponse(): ?LoggedResponse
{ {
return $this->parsedResponse; return $this->response;
} }
public function getPostData() public function getPostData()
@@ -280,7 +174,7 @@ class LoggedRequest implements \JsonSerializable
return $postData; return $postData;
} }
protected function detectSubdomain() public function detectSubdomain()
{ {
return collect($this->parsedRequest->getHeaders()->toArray()) return collect($this->parsedRequest->getHeaders()->toArray())
->mapWithKeys(function ($value, $key) { ->mapWithKeys(function ($value, $key) {
@@ -296,6 +190,11 @@ class LoggedRequest implements \JsonSerializable
})->get('x-expose-request-id', (string) Str::uuid()); })->get('x-expose-request-id', (string) Str::uuid());
} }
public function getStartTime()
{
return $this->startTime;
}
public function getDuration() public function getDuration()
{ {
return $this->startTime->diffInMilliseconds($this->stopTime, false); return $this->startTime->diffInMilliseconds($this->stopTime, false);
@@ -303,10 +202,35 @@ class LoggedRequest implements \JsonSerializable
protected function getRequestAsCurl(): string protected function getRequestAsCurl(): string
{ {
$maxRequestLength = 256000;
if (strlen($this->rawRequest) > $maxRequestLength) {
return '';
}
try { try {
return (new CurlFormatter())->format(parse_request($this->rawRequest)); return (new CurlFormatter())->format(parse_request($this->rawRequest));
} catch (\Throwable $e) { } catch (\Throwable $e) {
return ''; return '';
} }
} }
public function getUrl()
{
$request = Message::parseRequest($this->rawRequest);
dd($request->getUri()->withFragment(''));
}
public function refreshId()
{
$requestId = (string) Str::uuid();
$this->getRequest()->getHeaders()->removeHeader(
$this->getRequest()->getHeader('x-expose-request-id')
);
$this->getRequest()->getHeaders()->addHeader(new GenericHeader('x-expose-request-id', $requestId));
$this->id = $requestId;
}
} }

View File

@@ -0,0 +1,160 @@
<?php
namespace App\Logger;
use Illuminate\Support\Str;
use Laminas\Http\Request;
use Laminas\Http\Response;
class LoggedResponse
{
/** @var string */
protected $rawResponse;
/** @var Response */
protected $response;
/** @var Request */
protected $request;
protected $reasonPhrase;
protected $body;
protected $statusCode;
protected $headers;
public function __construct(string $rawResponse, Response $response, Request $request)
{
$this->rawResponse = $rawResponse;
$this->response = $response;
$this->request = $request;
if (! $this->shouldReturnBody()) {
$this->rawResponse = 'SKIPPED BY CONFIG OR BINARY RESPONSE';
$this->body = 'SKIPPED BY CONFIG OR BINARY RESPONSE';
} else {
try {
$this->body = $response->getBody();
} catch (\Exception $e) {
$this->body = '';
}
}
$this->statusCode = $response->getStatusCode();
$this->reasonPhrase = $response->getReasonPhrase();
$this->headers = $response->getHeaders()->toArray();
$this->response = null;
$this->request = null;
}
protected function shouldReturnBody(): bool
{
if ($this->skipByStatus()) {
return false;
}
if ($this->skipByContentType()) {
return false;
}
if ($this->skipByExtension()) {
return false;
}
if ($this->skipBySize()) {
return false;
}
$header = $this->response->getHeaders()->get('Content-Type');
$contentType = $header ? $header->getMediaType() : '';
$patterns = [
'application/json',
'text/*',
'*javascript*',
];
return Str::is($patterns, $contentType);
}
protected function skipByStatus(): bool
{
if (empty(config()->get('expose.skip_body_log.status'))) {
return false;
}
return Str::is(config()->get('expose.skip_body_log.status'), $this->response->getStatusCode());
}
protected function skipByContentType(): bool
{
if (empty(config()->get('expose.skip_body_log.content_type'))) {
return false;
}
$header = $this->response->getHeaders()->get('Content-Type');
$contentType = $header ? $header->getMediaType() : '';
return Str::is(config()->get('expose.skip_body_log.content_type'), $contentType);
}
protected function skipByExtension(): bool
{
if (empty(config()->get('expose.skip_body_log.extension'))) {
return false;
}
return Str::is(config()->get('expose.skip_body_log.extension'), $this->request->getUri()->getPath());
}
protected function skipBySize(): bool
{
$configSize = $this->getConfigSize(config()->get('expose.skip_body_log.size', '1MB'));
$contentLength = $this->response->getHeaders()->get('Content-Length');
if (! $contentLength) {
return false;
}
$contentSize = $contentLength->getFieldValue() ?? 0;
return $contentSize > $configSize;
}
protected function getConfigSize(string $size): int
{
$units = ['B', 'KB', 'MB', 'GB'];
$number = substr($size, 0, -2);
$suffix = strtoupper(substr($size, -2));
// B or no suffix
if (is_numeric(substr($suffix, 0, 1))) {
return preg_replace('/[^\d]/', '', $size);
}
// if we have an error in the input, default to GB
$exponent = array_flip($units)[$suffix] ?? 5;
return $number * (1024 ** $exponent);
}
public function getStatusCode()
{
return $this->statusCode;
}
public function getReasonPhrase()
{
return $this->reasonPhrase;
}
public function toArray()
{
return [
'raw' => $this->rawResponse,
'status' => $this->statusCode,
'headers' => $this->headers,
'reason' => $this->reasonPhrase,
'body' => $this->body,
];
}
}

View File

@@ -6,6 +6,8 @@ use App\Logger\CliRequestLogger;
use App\Logger\RequestLogger; use App\Logger\RequestLogger;
use Clue\React\Buzz\Browser; use Clue\React\Buzz\Browser;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Laminas\Uri\Uri;
use Laminas\Uri\UriFactory;
use React\EventLoop\Factory as LoopFactory; use React\EventLoop\Factory as LoopFactory;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
@@ -13,7 +15,8 @@ class AppServiceProvider extends ServiceProvider
{ {
public function boot() public function boot()
{ {
// UriFactory::registerScheme('capacitor', Uri::class);
UriFactory::registerScheme('chrome-extension', Uri::class);
} }
public function register() public function register()
@@ -35,6 +38,14 @@ class AppServiceProvider extends ServiceProvider
{ {
$builtInConfig = config('expose'); $builtInConfig = config('expose');
$keyServerVariable = 'EXPOSE_CONFIG_FILE';
if (array_key_exists($keyServerVariable, $_SERVER) && is_string($_SERVER[$keyServerVariable]) && file_exists($_SERVER[$keyServerVariable])) {
$localConfig = require $_SERVER[$keyServerVariable];
config()->set('expose', array_merge($builtInConfig, $localConfig));
return;
}
$localConfigFile = getcwd().DIRECTORY_SEPARATOR.'.expose.php'; $localConfigFile = getcwd().DIRECTORY_SEPARATOR.'.expose.php';
if (file_exists($localConfigFile)) { if (file_exists($localConfigFile)) {

View File

@@ -40,6 +40,7 @@ class Configuration implements \JsonSerializable
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
#[\ReturnTypeWillChange]
public function jsonSerialize() public function jsonSerialize()
{ {
return array_merge([ return array_merge([

View File

@@ -3,9 +3,14 @@
namespace App\Server\Connections; namespace App\Server\Connections;
use App\Contracts\ConnectionManager as ConnectionManagerContract; use App\Contracts\ConnectionManager as ConnectionManagerContract;
use App\Contracts\LoggerRepository;
use App\Contracts\StatisticsCollector;
use App\Contracts\SubdomainGenerator; use App\Contracts\SubdomainGenerator;
use App\Http\QueryParameters;
use App\Server\Exceptions\NoFreePortAvailable;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
use React\Socket\Server;
class ConnectionManager implements ConnectionManagerContract class ConnectionManager implements ConnectionManagerContract
{ {
@@ -21,10 +26,18 @@ class ConnectionManager implements ConnectionManagerContract
/** @var LoopInterface */ /** @var LoopInterface */
protected $loop; protected $loop;
public function __construct(SubdomainGenerator $subdomainGenerator, LoopInterface $loop) /** @var StatisticsCollector */
protected $statisticsCollector;
/** @var LoggerRepository */
protected $logger;
public function __construct(SubdomainGenerator $subdomainGenerator, StatisticsCollector $statisticsCollector, LoggerRepository $logger, LoopInterface $loop)
{ {
$this->subdomainGenerator = $subdomainGenerator; $this->subdomainGenerator = $subdomainGenerator;
$this->loop = $loop; $this->loop = $loop;
$this->statisticsCollector = $statisticsCollector;
$this->logger = $logger;
} }
public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength) public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength)
@@ -40,19 +53,86 @@ class ConnectionManager implements ConnectionManagerContract
}); });
} }
public function storeConnection(string $host, ?string $subdomain, ConnectionInterface $connection): ControlConnection public function storeConnection(string $host, ?string $subdomain, ?string $serverHost, ConnectionInterface $connection): ControlConnection
{ {
$clientId = (string) uniqid(); $clientId = (string) uniqid();
$connection->client_id = $clientId; $connection->client_id = $clientId;
$storedConnection = new ControlConnection($connection, $host, $subdomain ?? $this->subdomainGenerator->generateSubdomain(), $clientId); $storedConnection = new ControlConnection(
$connection,
$host,
$subdomain ?? $this->subdomainGenerator->generateSubdomain(),
$clientId,
$serverHost,
$this->getAuthTokenFromConnection($connection)
);
$this->connections[] = $storedConnection; $this->connections[] = $storedConnection;
$this->statisticsCollector->siteShared($this->getAuthTokenFromConnection($connection));
$this->logger->logSubdomain($storedConnection->authToken, $storedConnection->subdomain);
$this->performConnectionCallback($storedConnection);
return $storedConnection; return $storedConnection;
} }
protected function performConnectionCallback(ControlConnection $connection)
{
$connectionCallback = config('expose.admin.connection_callback');
if ($connectionCallback !== null && class_exists($connectionCallback)) {
app($connectionCallback)->handle($connection);
}
}
public function storeTcpConnection(int $port, ConnectionInterface $connection): ControlConnection
{
$clientId = (string) uniqid();
$connection->client_id = $clientId;
$storedConnection = new TcpControlConnection(
$connection,
$port,
$this->getSharedTcpServer(),
$clientId,
$this->getAuthTokenFromConnection($connection)
);
$this->connections[] = $storedConnection;
$this->statisticsCollector->portShared($this->getAuthTokenFromConnection($connection));
return $storedConnection;
}
protected function getSharedTcpServer(): Server
{
$portRange = config('expose.admin.tcp_port_range');
$port = $portRange['from'] ?? 50000;
$maxPort = $portRange['to'] ?? 60000;
do {
try {
$portFound = true;
$server = new Server('0.0.0.0:'.$port, $this->loop);
} catch (\RuntimeException $exception) {
$portFound = false;
$port++;
if ($port > $maxPort) {
throw new NoFreePortAvailable();
}
}
} while (! $portFound);
return $server;
}
public function storeHttpConnection(ConnectionInterface $httpConnection, $requestId): HttpConnection public function storeHttpConnection(ConnectionInterface $httpConnection, $requestId): HttpConnection
{ {
$this->httpConnections[$requestId] = new HttpConnection($httpConnection); $this->httpConnections[$requestId] = new HttpConnection($httpConnection);
@@ -75,16 +155,26 @@ class ConnectionManager implements ConnectionManagerContract
if (isset($connection->client_id)) { if (isset($connection->client_id)) {
$clientId = $connection->client_id; $clientId = $connection->client_id;
$controlConnection = collect($this->connections)->first(function ($connection) use ($clientId) {
return $connection->client_id == $clientId;
});
if ($controlConnection instanceof TcpControlConnection) {
$controlConnection->stop();
$controlConnection = null;
}
$this->connections = collect($this->connections)->reject(function ($connection) use ($clientId) { $this->connections = collect($this->connections)->reject(function ($connection) use ($clientId) {
return $connection->client_id == $clientId; return $connection->client_id == $clientId;
})->toArray(); })->toArray();
} }
} }
public function findControlConnectionForSubdomain($subdomain): ?ControlConnection public function findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost): ?ControlConnection
{ {
return collect($this->connections)->last(function ($connection) use ($subdomain) { return collect($this->connections)->last(function ($connection) use ($subdomain, $serverHost) {
return $connection->subdomain == $subdomain; return $connection->subdomain == $subdomain && $connection->serverHost === $serverHost;
}); });
} }
@@ -95,8 +185,59 @@ class ConnectionManager implements ConnectionManagerContract
}); });
} }
public function findControlConnectionsForIp(string $ip): array
{
return collect($this->connections)->filter(function (ControlConnection $connection) use ($ip) {
return $connection->socket->remoteAddress == $ip;
})->toArray();
}
public function findControlConnectionsForAuthToken(string $token): array
{
return collect($this->connections)->filter(function (ControlConnection $connection) use ($token) {
return $connection->authToken === $token;
})->toArray();
}
public function getConnections(): array public function getConnections(): array
{ {
return $this->connections; return $this->connections;
} }
protected function getAuthTokenFromConnection(ConnectionInterface $connection): string
{
return QueryParameters::create($connection->httpRequest)->get('authToken');
}
public function getConnectionsForAuthToken(string $authToken): array
{
return collect($this->connections)
->filter(function ($connection) use ($authToken) {
return $connection->authToken === $authToken;
})
->filter(function ($connection) {
return get_class($connection) === ControlConnection::class;
})
->map(function ($connection) {
return $connection->toArray();
})
->values()
->toArray();
}
public function getTcpConnectionsForAuthToken(string $authToken): array
{
return collect($this->connections)
->filter(function ($connection) use ($authToken) {
return $connection->authToken === $authToken;
})
->filter(function ($connection) {
return get_class($connection) === TcpControlConnection::class;
})
->map(function ($connection) {
return $connection->toArray();
})
->values()
->toArray();
}
} }

View File

@@ -2,6 +2,7 @@
namespace App\Server\Connections; namespace App\Server\Connections;
use App\Http\QueryParameters;
use Evenement\EventEmitterTrait; use Evenement\EventEmitterTrait;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
@@ -12,18 +13,24 @@ class ControlConnection
/** @var ConnectionInterface */ /** @var ConnectionInterface */
public $socket; public $socket;
public $host; public $host;
public $serverHost;
public $authToken;
public $subdomain; public $subdomain;
public $client_id; public $client_id;
public $client_version;
public $proxies = []; public $proxies = [];
protected $shared_at; protected $shared_at;
public function __construct(ConnectionInterface $socket, string $host, string $subdomain, string $clientId) public function __construct(ConnectionInterface $socket, string $host, string $subdomain, string $clientId, string $serverHost, string $authToken = '')
{ {
$this->socket = $socket; $this->socket = $socket;
$this->host = $host; $this->host = $host;
$this->subdomain = $subdomain; $this->subdomain = $subdomain;
$this->client_id = $clientId; $this->client_id = $clientId;
$this->authToken = $authToken;
$this->serverHost = $serverHost;
$this->shared_at = now()->toDateTimeString(); $this->shared_at = now()->toDateTimeString();
$this->client_version = QueryParameters::create($socket->httpRequest)->get('version');
} }
public function setMaximumConnectionLength(int $maximumConnectionLength) public function setMaximumConnectionLength(int $maximumConnectionLength)
@@ -41,6 +48,8 @@ class ControlConnection
$this->socket->send(json_encode([ $this->socket->send(json_encode([
'event' => 'createProxy', 'event' => 'createProxy',
'data' => [ 'data' => [
'host' => $this->host,
'subdomain' => $this->subdomain,
'request_id' => $requestId, 'request_id' => $requestId,
'client_id' => $this->client_id, 'client_id' => $this->client_id,
], ],
@@ -55,8 +64,13 @@ class ControlConnection
public function toArray() public function toArray()
{ {
return [ return [
'type' => 'http',
'host' => $this->host, 'host' => $this->host,
'remote_address' => $this->socket->remoteAddress ?? null,
'server_host' => $this->serverHost,
'client_id' => $this->client_id, 'client_id' => $this->client_id,
'client_version' => $this->client_version,
'auth_token' => $this->authToken,
'subdomain' => $this->subdomain, 'subdomain' => $this->subdomain,
'shared_at' => $this->shared_at, 'shared_at' => $this->shared_at,
]; ];

View File

@@ -0,0 +1,108 @@
<?php
namespace App\Server\Connections;
use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\Frame;
use React\Socket\Server;
class TcpControlConnection extends ControlConnection
{
public $proxy;
public $proxyConnection;
public $port;
public $shared_port;
public $shared_server;
public function __construct(ConnectionInterface $socket, int $port, Server $sharedServer, string $clientId, string $authToken = '')
{
$this->socket = $socket;
$this->client_id = $clientId;
$this->shared_server = $sharedServer;
$this->port = $port;
$this->shared_at = now()->toDateTimeString();
$this->shared_port = parse_url($sharedServer->getAddress(), PHP_URL_PORT);
$this->authToken = $authToken;
$this->configureServer($sharedServer);
}
public function setMaximumConnectionLength(int $maximumConnectionLength)
{
$this->socket->send(json_encode([
'event' => 'setMaximumConnectionLength',
'data' => [
'length' => $maximumConnectionLength,
],
]));
}
public function registerProxy($requestId)
{
$this->socket->send(json_encode([
'event' => 'createProxy',
'data' => [
'request_id' => $requestId,
'client_id' => $this->client_id,
],
]));
}
public function registerTcpProxy($requestId)
{
$this->socket->send(json_encode([
'event' => 'createTcpProxy',
'data' => [
'port' => $this->port,
'tcp_request_id' => $requestId,
'client_id' => $this->client_id,
],
]));
}
public function stop()
{
$this->shared_server->close();
$this->shared_server = null;
}
public function close()
{
$this->socket->close();
}
public function toArray()
{
return [
'type' => 'tcp',
'port' => $this->port,
'auth_token' => $this->authToken,
'client_id' => $this->client_id,
'shared_port' => $this->shared_port,
'shared_at' => $this->shared_at,
];
}
protected function configureServer(Server $sharedServer)
{
$requestId = uniqid();
$sharedServer->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($requestId) {
$this->proxyConnection = $connection;
$this->once('tcp_proxy_ready_'.$requestId, function (ConnectionInterface $proxy) use ($connection) {
$this->proxy = $proxy;
$connection->on('data', function ($data) use ($proxy) {
$binaryMsg = new Frame($data, true, Frame::OP_BINARY);
$proxy->send($binaryMsg);
});
$connection->resume();
});
$connection->pause();
$this->registerTcpProxy($requestId);
});
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace App\Server\DomainRepository;
use App\Contracts\DomainRepository;
use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
class DatabaseDomainRepository implements DomainRepository
{
/** @var DatabaseInterface */
protected $database;
public function __construct(DatabaseInterface $database)
{
$this->database = $database;
}
public function getDomains(): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM domains ORDER by created_at DESC')
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows);
});
return $deferred->promise();
}
public function getDomainById($id): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM domains WHERE id = :id', ['id' => $id])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0] ?? null);
});
return $deferred->promise();
}
public function getDomainByName(string $name): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM domains WHERE domain = :name', ['name' => $name])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0] ?? null);
});
return $deferred->promise();
}
public function getDomainsByUserId($id): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM domains WHERE user_id = :user_id ORDER by created_at DESC', [
'user_id' => $id,
])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows);
});
return $deferred->promise();
}
public function storeDomain(array $data): PromiseInterface
{
$deferred = new Deferred();
$this->getDomainByName($data['domain'])
->then(function ($registeredDomain) use ($data, $deferred) {
$this->database->query("
INSERT INTO domains (user_id, domain, created_at)
VALUES (:user_id, :domain, DATETIME('now'))
", $data)
->then(function (Result $result) use ($deferred) {
$this->database->query('SELECT * FROM domains WHERE id = :id', ['id' => $result->insertId])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0]);
});
});
});
return $deferred->promise();
}
public function getDomainsByUserIdAndName($id, $name): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM domains WHERE user_id = :user_id AND domain = :name ORDER by created_at DESC', [
'user_id' => $id,
'name' => $name,
])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows);
});
return $deferred->promise();
}
public function deleteDomainForUserId($userId, $domainId): PromiseInterface
{
$deferred = new Deferred();
$this->database->query('DELETE FROM domains WHERE id = :id AND user_id = :user_id', [
'id' => $domainId,
'user_id' => $userId,
])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result);
});
return $deferred->promise();
}
public function updateDomain($id, array $data): PromiseInterface
{
$deferred = new Deferred();
// TODO
return $deferred->promise();
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Server\Exceptions;
class NoFreePortAvailable extends \Exception
{
}

View File

@@ -3,25 +3,46 @@
namespace App\Server; namespace App\Server;
use App\Contracts\ConnectionManager as ConnectionManagerContract; use App\Contracts\ConnectionManager as ConnectionManagerContract;
use App\Contracts\DomainRepository;
use App\Contracts\LoggerRepository;
use App\Contracts\StatisticsCollector;
use App\Contracts\StatisticsRepository;
use App\Contracts\SubdomainGenerator; use App\Contracts\SubdomainGenerator;
use App\Contracts\SubdomainRepository;
use App\Contracts\UserRepository; use App\Contracts\UserRepository;
use App\Http\RouteGenerator; use App\Http\RouteGenerator;
use App\Http\Server as HttpServer; use App\Http\Server as HttpServer;
use App\Server\Connections\ConnectionManager; use App\Server\Connections\ConnectionManager;
use App\Server\DomainRepository\DatabaseDomainRepository;
use App\Server\Http\Controllers\Admin\DeleteSubdomainController;
use App\Server\Http\Controllers\Admin\DeleteUsersController; use App\Server\Http\Controllers\Admin\DeleteUsersController;
use App\Server\Http\Controllers\Admin\DisconnectSiteController; use App\Server\Http\Controllers\Admin\DisconnectSiteController;
use App\Server\Http\Controllers\Admin\DisconnectTcpConnectionController;
use App\Server\Http\Controllers\Admin\GetLogsController;
use App\Server\Http\Controllers\Admin\GetLogsForSubdomainController;
use App\Server\Http\Controllers\Admin\GetSettingsController; use App\Server\Http\Controllers\Admin\GetSettingsController;
use App\Server\Http\Controllers\Admin\GetSiteDetailsController;
use App\Server\Http\Controllers\Admin\GetSitesController; use App\Server\Http\Controllers\Admin\GetSitesController;
use App\Server\Http\Controllers\Admin\GetStatisticsController;
use App\Server\Http\Controllers\Admin\GetTcpConnectionsController;
use App\Server\Http\Controllers\Admin\GetUserDetailsController;
use App\Server\Http\Controllers\Admin\GetUsersController; use App\Server\Http\Controllers\Admin\GetUsersController;
use App\Server\Http\Controllers\Admin\ListSitesController; use App\Server\Http\Controllers\Admin\ListSitesController;
use App\Server\Http\Controllers\Admin\ListTcpConnectionsController;
use App\Server\Http\Controllers\Admin\ListUsersController; use App\Server\Http\Controllers\Admin\ListUsersController;
use App\Server\Http\Controllers\Admin\RedirectToUsersController; use App\Server\Http\Controllers\Admin\RedirectToUsersController;
use App\Server\Http\Controllers\Admin\ShowSettingsController; use App\Server\Http\Controllers\Admin\ShowSettingsController;
use App\Server\Http\Controllers\Admin\StoreDomainController;
use App\Server\Http\Controllers\Admin\StoreSettingsController; use App\Server\Http\Controllers\Admin\StoreSettingsController;
use App\Server\Http\Controllers\Admin\StoreSubdomainController;
use App\Server\Http\Controllers\Admin\StoreUsersController; use App\Server\Http\Controllers\Admin\StoreUsersController;
use App\Server\Http\Controllers\ControlMessageController; use App\Server\Http\Controllers\ControlMessageController;
use App\Server\Http\Controllers\TunnelMessageController; use App\Server\Http\Controllers\TunnelMessageController;
use App\Server\Http\Router; use App\Server\Http\Router;
use App\Server\LoggerRepository\NullLogger;
use App\Server\StatisticsCollector\DatabaseStatisticsCollector;
use App\Server\StatisticsRepository\DatabaseStatisticsRepository;
use App\Server\SubdomainRepository\DatabaseSubdomainRepository;
use Clue\React\SQLite\DatabaseInterface; use Clue\React\SQLite\DatabaseInterface;
use Phar; use Phar;
use Ratchet\Server\IoServer; use Ratchet\Server\IoServer;
@@ -119,14 +140,32 @@ class Factory
$this->router->get('/users', ListUsersController::class, $adminCondition); $this->router->get('/users', ListUsersController::class, $adminCondition);
$this->router->get('/settings', ShowSettingsController::class, $adminCondition); $this->router->get('/settings', ShowSettingsController::class, $adminCondition);
$this->router->get('/sites', ListSitesController::class, $adminCondition); $this->router->get('/sites', ListSitesController::class, $adminCondition);
$this->router->get('/tcp', ListTcpConnectionsController::class, $adminCondition);
$this->router->get('/api/statistics', GetStatisticsController::class, $adminCondition);
$this->router->get('/api/settings', GetSettingsController::class, $adminCondition); $this->router->get('/api/settings', GetSettingsController::class, $adminCondition);
$this->router->post('/api/settings', StoreSettingsController::class, $adminCondition); $this->router->post('/api/settings', StoreSettingsController::class, $adminCondition);
$this->router->get('/api/users', GetUsersController::class, $adminCondition); $this->router->get('/api/users', GetUsersController::class, $adminCondition);
$this->router->post('/api/users', StoreUsersController::class, $adminCondition); $this->router->post('/api/users', StoreUsersController::class, $adminCondition);
$this->router->get('/api/users/{id}', GetUserDetailsController::class, $adminCondition);
$this->router->delete('/api/users/{id}', DeleteUsersController::class, $adminCondition); $this->router->delete('/api/users/{id}', DeleteUsersController::class, $adminCondition);
$this->router->get('/api/logs', GetLogsController::class, $adminCondition);
$this->router->get('/api/logs/{subdomain}', GetLogsForSubdomainController::class, $adminCondition);
$this->router->post('/api/domains', StoreDomainController::class, $adminCondition);
$this->router->delete('/api/domains/{domain}', DeleteSubdomainController::class, $adminCondition);
$this->router->post('/api/subdomains', StoreSubdomainController::class, $adminCondition);
$this->router->delete('/api/subdomains/{subdomain}', DeleteSubdomainController::class, $adminCondition);
$this->router->get('/api/sites', GetSitesController::class, $adminCondition); $this->router->get('/api/sites', GetSitesController::class, $adminCondition);
$this->router->get('/api/sites/{site}', GetSiteDetailsController::class, $adminCondition);
$this->router->delete('/api/sites/{id}', DisconnectSiteController::class, $adminCondition); $this->router->delete('/api/sites/{id}', DisconnectSiteController::class, $adminCondition);
$this->router->get('/api/tcp', GetTcpConnectionsController::class, $adminCondition);
$this->router->delete('/api/tcp/{id}', DisconnectTcpConnectionController::class, $adminCondition);
} }
protected function bindConfiguration() protected function bindConfiguration()
@@ -163,8 +202,12 @@ class Factory
$this->bindConfiguration() $this->bindConfiguration()
->bindSubdomainGenerator() ->bindSubdomainGenerator()
->bindUserRepository() ->bindUserRepository()
->bindLoggerRepository()
->bindSubdomainRepository()
->bindDomainRepository()
->bindDatabase() ->bindDatabase()
->ensureDatabaseIsInitialized() ->ensureDatabaseIsInitialized()
->registerStatisticsCollector()
->bindConnectionManager() ->bindConnectionManager()
->addAdminRoutes(); ->addAdminRoutes();
@@ -199,6 +242,33 @@ class Factory
return $this; return $this;
} }
protected function bindSubdomainRepository()
{
app()->singleton(SubdomainRepository::class, function () {
return app(config('expose.admin.subdomain_repository', DatabaseSubdomainRepository::class));
});
return $this;
}
protected function bindLoggerRepository()
{
app()->singleton(LoggerRepository::class, function () {
return app(config('expose.admin.logger_repository', NullLogger::class));
});
return $this;
}
protected function bindDomainRepository()
{
app()->singleton(DomainRepository::class, function () {
return app(config('expose.admin.domain_repository', DatabaseDomainRepository::class));
});
return $this;
}
protected function bindDatabase() protected function bindDatabase()
{ {
app()->singleton(DatabaseInterface::class, function () { app()->singleton(DatabaseInterface::class, function () {
@@ -225,7 +295,8 @@ class Factory
->files() ->files()
->ignoreDotFiles(true) ->ignoreDotFiles(true)
->in(database_path('migrations')) ->in(database_path('migrations'))
->name('*.sql'); ->name('*.sql')
->sortByName();
/** @var SplFileInfo $migration */ /** @var SplFileInfo $migration */
foreach ($migrations as $migration) { foreach ($migrations as $migration) {
@@ -241,4 +312,27 @@ class Factory
return $this; return $this;
} }
protected function registerStatisticsCollector()
{
if (config('expose.admin.statistics.enable_statistics', true) === false) {
return $this;
}
app()->singleton(StatisticsRepository::class, function () {
return app(config('expose.admin.statistics.repository', DatabaseStatisticsRepository::class));
});
app()->singleton(StatisticsCollector::class, function () {
return app(DatabaseStatisticsCollector::class);
});
$intervalInSeconds = config('expose.admin.statistics.interval_in_seconds', 3600);
$this->loop->addPeriodicTimer($intervalInSeconds, function () {
app(StatisticsCollector::class)->save();
});
return $this;
}
} }

View File

@@ -3,8 +3,8 @@
namespace App\Server\Http\Controllers\Admin; namespace App\Server\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use function GuzzleHttp\Psr7\str;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
@@ -14,7 +14,7 @@ abstract class AdminController extends Controller
protected function shouldHandleRequest(Request $request, ConnectionInterface $httpConnection): bool protected function shouldHandleRequest(Request $request, ConnectionInterface $httpConnection): bool
{ {
try { try {
$authorization = Str::after($request->header('Authorization'), 'Basic '); $authorization = Str::after($request->header('Authorization', ''), 'Basic ');
$authParts = explode(':', base64_decode($authorization), 2); $authParts = explode(':', base64_decode($authorization), 2);
[$user, $password] = $authParts; [$user, $password] = $authParts;
@@ -24,9 +24,11 @@ abstract class AdminController extends Controller
return true; return true;
} catch (\Throwable $e) { } catch (\Throwable $e) {
$httpConnection->send(str(new Response(401, [ $httpConnection->send(Message::toString(new Response(401, [
'WWW-Authenticate' => 'Basic realm="Expose"', 'WWW-Authenticate' => 'Basic realm="Expose"',
]))); ])));
$httpConnection->close();
} }
return false; return false;

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\SubdomainRepository;
use App\Contracts\UserRepository;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class DeleteSubdomainController extends AdminController
{
protected $keepConnectionOpen = true;
/** @var SubdomainRepository */
protected $subdomainRepository;
/** @var UserRepository */
protected $userRepository;
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository)
{
$this->userRepository = $userRepository;
$this->subdomainRepository = $subdomainRepository;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$this->userRepository->getUserByToken($request->get('auth_token', ''))
->then(function ($user) use ($request, $httpConnection) {
if (is_null($user)) {
$httpConnection->send(respond_json(['error' => 'The user does not exist'], 404));
$httpConnection->close();
return;
}
$this->subdomainRepository->deleteSubdomainForUserId($user['id'], $request->get('subdomain'))
->then(function ($deleted) use ($httpConnection) {
$httpConnection->send(respond_json(['deleted' => $deleted], 200));
$httpConnection->close();
});
});
}
}

View File

@@ -22,7 +22,11 @@ class DisconnectSiteController extends AdminController
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
{ {
$connection = $this->connectionManager->findControlConnectionForClientId($request->get('id')); if ($request->has('server_host')) {
$connection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($request->get('id'), $request->get('server_host'));
} else {
$connection = $this->connectionManager->findControlConnectionForClientId($request->get('id'));
}
if (! is_null($connection)) { if (! is_null($connection)) {
$connection->close(); $connection->close();

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\ConnectionManager;
use App\Server\Configuration;
use App\Server\Connections\TcpControlConnection;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class DisconnectTcpConnectionController extends AdminController
{
/** @var ConnectionManager */
protected $connectionManager;
/** @var Configuration */
protected $configuration;
public function __construct(ConnectionManager $connectionManager)
{
$this->connectionManager = $connectionManager;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$connection = $this->connectionManager->findControlConnectionForClientId($request->get('id'));
if (! is_null($connection)) {
$connection->close();
$this->connectionManager->removeControlConnection($connection);
}
$httpConnection->send(respond_json([
'tcp_connections' => collect($this->connectionManager->getConnections())
->filter(function ($connection) {
return get_class($connection) === TcpControlConnection::class;
})
->values(),
]));
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\LoggerRepository;
use App\Server\Configuration;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class GetLogsController extends AdminController
{
protected $keepConnectionOpen = true;
/** @var Configuration */
protected $configuration;
/** @var LoggerRepository */
protected $logger;
public function __construct(LoggerRepository $logger)
{
$this->logger = $logger;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$subdomain = $request->get('subdomain');
$this->logger->getLogs()
->then(function ($logs) use ($httpConnection) {
$httpConnection->send(
respond_json(['logs' => $logs])
);
$httpConnection->close();
});
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\LoggerRepository;
use App\Server\Configuration;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class GetLogsForSubdomainController extends AdminController
{
protected $keepConnectionOpen = true;
/** @var Configuration */
protected $configuration;
/** @var LoggerRepository */
protected $logger;
public function __construct(LoggerRepository $logger)
{
$this->logger = $logger;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$subdomain = $request->get('subdomain');
$this->logger->getLogsBySubdomain($subdomain)
->then(function ($logs) use ($httpConnection) {
$httpConnection->send(
respond_json(['logs' => $logs])
);
$httpConnection->close();
});
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\ConnectionManager;
use App\Server\Configuration;
use App\Server\Connections\ControlConnection;
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Response;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class GetSiteDetailsController extends AdminController
{
/** @var ConnectionManager */
protected $connectionManager;
/** @var Configuration */
protected $configuration;
public function __construct(ConnectionManager $connectionManager, Configuration $configuration)
{
$this->connectionManager = $connectionManager;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$domain = $request->get('site');
$connectedSite = collect($this->connectionManager->getConnections())
->filter(function ($connection) {
return get_class($connection) === ControlConnection::class;
})
->first(function (ControlConnection $site) use ($domain) {
return "{$site->subdomain}.{$site->serverHost}" === $domain;
});
if (is_null($connectedSite)) {
$httpConnection->send(
Message::toString(new Response(404))
);
return;
}
$httpConnection->send(
respond_json($connectedSite->toArray())
);
}
}

View File

@@ -3,33 +3,63 @@
namespace App\Server\Http\Controllers\Admin; namespace App\Server\Http\Controllers\Admin;
use App\Contracts\ConnectionManager; use App\Contracts\ConnectionManager;
use App\Contracts\UserRepository;
use App\Server\Configuration; use App\Server\Configuration;
use App\Server\Connections\ControlConnection;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
class GetSitesController extends AdminController class GetSitesController extends AdminController
{ {
protected $keepConnectionOpen = true;
/** @var ConnectionManager */ /** @var ConnectionManager */
protected $connectionManager; protected $connectionManager;
/** @var Configuration */ /** @var Configuration */
protected $configuration; protected $configuration;
public function __construct(ConnectionManager $connectionManager, Configuration $configuration) /** @var UserRepository */
protected $userRepository;
public function __construct(ConnectionManager $connectionManager, Configuration $configuration, UserRepository $userRepository)
{ {
$this->connectionManager = $connectionManager; $this->connectionManager = $connectionManager;
$this->userRepository = $userRepository;
} }
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
{ {
$httpConnection->send( $authTokens = [];
respond_json([
'sites' => collect($this->connectionManager->getConnections())->map(function ($site, $siteId) { $sites = collect($this->connectionManager->getConnections())
$site = $site->toArray(); ->filter(function ($connection) {
$site['id'] = $siteId; return get_class($connection) === ControlConnection::class;
})
->map(function ($site, $siteId) use (&$authTokens) {
$site = $site->toArray();
$site['id'] = $siteId;
$authTokens[] = $site['auth_token'];
return $site;
})->values();
$this->userRepository->getUsersByTokens($authTokens)
->then(function ($users) use ($httpConnection, $sites) {
$users = collect($users);
$sites = collect($sites)->map(function ($site) use ($users) {
$site['user'] = $users->firstWhere('auth_token', $site['auth_token']);
return $site; return $site;
})->values(), })->toArray();
])
); $httpConnection->send(
respond_json([
'sites' => $sites,
])
);
$httpConnection->close();
});
} }
} }

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\StatisticsRepository;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class GetStatisticsController extends AdminController
{
protected $keepConnectionOpen = true;
/** @var StatisticsRepository */
protected $statisticsRepository;
public function __construct(StatisticsRepository $statisticsRepository)
{
$this->statisticsRepository = $statisticsRepository;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$from = today()->subWeek()->toDateString();
$until = today()->toDateString();
$this->statisticsRepository->getStatistics($request->get('from', $from), $request->get('until', $until))
->then(function ($statistics) use ($httpConnection) {
$httpConnection->send(
respond_json([
'statistics' => $statistics,
])
);
$httpConnection->close();
});
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\ConnectionManager;
use App\Contracts\UserRepository;
use App\Server\Configuration;
use App\Server\Connections\TcpControlConnection;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class GetTcpConnectionsController extends AdminController
{
protected $keepConnectionOpen = true;
/** @var ConnectionManager */
protected $connectionManager;
/** @var Configuration */
protected $configuration;
/** @var UserRepository */
protected $userRepository;
public function __construct(ConnectionManager $connectionManager, Configuration $configuration, UserRepository $userRepository)
{
$this->connectionManager = $connectionManager;
$this->userRepository = $userRepository;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$authTokens = [];
$connections = collect($this->connectionManager->getConnections())
->filter(function ($connection) {
return get_class($connection) === TcpControlConnection::class;
})
->map(function ($site, $siteId) use (&$authTokens) {
$site = $site->toArray();
$site['id'] = $siteId;
$authTokens[] = $site['auth_token'];
return $site;
})
->values();
$this->userRepository->getUsersByTokens($authTokens)
->then(function ($users) use ($httpConnection, $connections) {
$users = collect($users);
$connections = collect($connections)->map(function ($connection) use ($users) {
$connection['user'] = $users->firstWhere('auth_token', $connection['auth_token']);
return $connection;
})->toArray();
$httpConnection->send(
respond_json([
'tcp_connections' => $connections,
])
);
$httpConnection->close();
});
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\SubdomainRepository;
use App\Contracts\UserRepository;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class GetUserDetailsController extends AdminController
{
protected $keepConnectionOpen = true;
/** @var UserRepository */
protected $userRepository;
/** @var SubdomainRepository */
protected $subdomainRepository;
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository)
{
$this->userRepository = $userRepository;
$this->subdomainRepository = $subdomainRepository;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$id = $request->get('id');
if (! is_numeric($id)) {
$promise = $this->userRepository->getUserByToken($id);
} else {
$promise = $this->userRepository->getUserById($id);
}
$promise->then(function ($user) use ($httpConnection) {
if (is_null($user)) {
$httpConnection->send(
respond_json([], 404)
);
$httpConnection->close();
return;
}
$this->subdomainRepository->getSubdomainsByUserId($user['id'])
->then(function ($subdomains) use ($httpConnection, $user) {
$httpConnection->send(
respond_json([
'user' => $user,
'subdomains' => $subdomains,
])
);
$httpConnection->close();
});
});
}
}

View File

@@ -21,10 +21,10 @@ class GetUsersController extends AdminController
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
{ {
$this->userRepository $this->userRepository
->getUsers() ->paginateUsers($request->get('search', ''), (int) $request->get('perPage', 20), (int) $request->get('page', 1))
->then(function ($users) use ($httpConnection) { ->then(function ($paginated) use ($httpConnection) {
$httpConnection->send( $httpConnection->send(
respond_json(['users' => $users]) respond_json(['paginated' => $paginated])
); );
$httpConnection->close(); $httpConnection->close();

View File

@@ -25,7 +25,6 @@ class ListSitesController extends AdminController
$sites = $this->getView($httpConnection, 'server.sites.index', [ $sites = $this->getView($httpConnection, 'server.sites.index', [
'scheme' => $this->configuration->port() === 443 ? 'https' : 'http', 'scheme' => $this->configuration->port() === 443 ? 'https' : 'http',
'configuration' => $this->configuration, 'configuration' => $this->configuration,
'sites' => $this->connectionManager->getConnections(),
]); ]);
$httpConnection->send( $httpConnection->send(

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\ConnectionManager;
use App\Server\Configuration;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class ListTcpConnectionsController extends AdminController
{
/** @var ConnectionManager */
protected $connectionManager;
/** @var Configuration */
protected $configuration;
public function __construct(ConnectionManager $connectionManager, Configuration $configuration)
{
$this->connectionManager = $connectionManager;
$this->configuration = $configuration;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$sites = $this->getView($httpConnection, 'server.tcp.index', [
'scheme' => $this->configuration->port() === 443 ? 'https' : 'http',
'configuration' => $this->configuration,
]);
$httpConnection->send(
respond_html($sites)
);
}
}

View File

@@ -21,10 +21,10 @@ class ListUsersController extends AdminController
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
{ {
$this->userRepository $this->userRepository
->getUsers() ->paginateUsers($request->get('search', ''), 20, (int) $request->get('page', 1))
->then(function ($users) use ($httpConnection) { ->then(function ($paginated) use ($httpConnection) {
$httpConnection->send( $httpConnection->send(
respond_html($this->getView($httpConnection, 'server.users.index', ['users' => $users])) respond_html($this->getView($httpConnection, 'server.users.index', ['paginated' => $paginated]))
); );
$httpConnection->close(); $httpConnection->close();

View File

@@ -2,17 +2,16 @@
namespace App\Server\Http\Controllers\Admin; namespace App\Server\Http\Controllers\Admin;
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use function GuzzleHttp\Psr7\str;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
class RedirectToUsersController extends AdminController class RedirectToUsersController extends AdminController
{ {
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
{ {
$httpConnection->send(str(new Response(301, [ $httpConnection->send(Message::toString(new Response(301, [
'Location' => '/sites', 'Location' => '/sites',
]))); ])));
} }

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\DomainRepository;
use App\Contracts\UserRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Ratchet\ConnectionInterface;
class StoreDomainController extends AdminController
{
protected $keepConnectionOpen = true;
/** @var DomainRepository */
protected $domainRepository;
/** @var UserRepository */
protected $userRepository;
public function __construct(UserRepository $userRepository, DomainRepository $domainRepository)
{
$this->userRepository = $userRepository;
$this->domainRepository = $domainRepository;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$validator = Validator::make($request->all(), [
'domain' => 'required',
], [
'required' => 'The :attribute field is required.',
]);
if ($validator->fails()) {
$httpConnection->send(respond_json(['errors' => $validator->getMessageBag()], 401));
$httpConnection->close();
return;
}
$this->userRepository
->getUserByToken($request->get('auth_token', ''))
->then(function ($user) use ($httpConnection, $request) {
if (is_null($user)) {
$httpConnection->send(respond_json(['error' => 'The user does not exist'], 404));
$httpConnection->close();
return;
}
if ($user['can_specify_domains'] === 0) {
$httpConnection->send(respond_json(['error' => 'The user is not allowed to reserve custom domains.'], 401));
$httpConnection->close();
return;
}
$insertData = [
'user_id' => $user['id'],
'domain' => $request->get('domain'),
];
$this->domainRepository
->storeDomain($insertData)
->then(function ($domain) use ($httpConnection) {
$httpConnection->send(respond_json(['domain' => $domain], 200));
$httpConnection->close();
});
});
}
}

View File

@@ -31,6 +31,14 @@ class StoreSettingsController extends AdminController
config()->set('expose.admin.messages.message_of_the_day', Arr::get($messages, 'message_of_the_day')); config()->set('expose.admin.messages.message_of_the_day', Arr::get($messages, 'message_of_the_day'));
config()->set('expose.admin.messages.custom_subdomain_unauthorized', Arr::get($messages, 'custom_subdomain_unauthorized'));
config()->set('expose.admin.messages.no_free_tcp_port_available', Arr::get($messages, 'no_free_tcp_port_available'));
config()->set('expose.admin.messages.tcp_port_sharing_unauthorized', Arr::get($messages, 'tcp_port_sharing_unauthorized'));
config()->set('expose.admin.messages.tcp_port_sharing_disabled', Arr::get($messages, 'tcp_port_sharing_disabled'));
$httpConnection->send( $httpConnection->send(
respond_json([ respond_json([
'configuration' => $this->configuration, 'configuration' => $this->configuration,

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\SubdomainRepository;
use App\Contracts\UserRepository;
use App\Server\Configuration;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Ratchet\ConnectionInterface;
class StoreSubdomainController extends AdminController
{
protected $keepConnectionOpen = true;
/** @var SubdomainRepository */
protected $subdomainRepository;
/** @var UserRepository */
protected $userRepository;
/** @var Configuration */
protected $configuration;
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository, Configuration $configuration)
{
$this->userRepository = $userRepository;
$this->subdomainRepository = $subdomainRepository;
$this->configuration = $configuration;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$validator = Validator::make($request->all(), [
'subdomain' => 'required',
], [
'required' => 'The :attribute field is required.',
]);
if ($validator->fails()) {
$httpConnection->send(respond_json(['errors' => $validator->getMessageBag()], 401));
$httpConnection->close();
return;
}
$this->userRepository
->getUserByToken($request->get('auth_token', ''))
->then(function ($user) use ($httpConnection, $request) {
if (is_null($user)) {
$httpConnection->send(respond_json(['error' => 'The user does not exist'], 404));
$httpConnection->close();
return;
}
if ($user['can_specify_subdomains'] === 0) {
$httpConnection->send(respond_json(['error' => 'The user is not allowed to reserve subdomains.'], 401));
$httpConnection->close();
return;
}
if (in_array($request->get('subdomain'), config('expose.admin.reserved_subdomains', []))) {
$httpConnection->send(respond_json(['error' => 'The subdomain is already taken.'], 422));
$httpConnection->close();
return;
}
$insertData = [
'user_id' => $user['id'],
'subdomain' => $request->get('subdomain'),
'domain' => $request->get('domain', $this->configuration->hostname()),
];
$this->subdomainRepository
->storeSubdomain($insertData)
->then(function ($subdomain) use ($httpConnection) {
$httpConnection->send(respond_json(['subdomain' => $subdomain], 200));
$httpConnection->close();
});
});
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Server\Http\Controllers\Admin; namespace App\Server\Http\Controllers\Admin;
use App\Contracts\UserRepository; use App\Contracts\UserRepository;
use function GuzzleHttp\Psr7\str;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -38,7 +37,11 @@ class StoreUsersController extends AdminController
$insertData = [ $insertData = [
'name' => $request->get('name'), 'name' => $request->get('name'),
'auth_token' => (string) Str::uuid(), 'auth_token' => $request->get('token', (string) Str::uuid()),
'can_specify_subdomains' => (int) $request->get('can_specify_subdomains'),
'can_specify_domains' => (int) $request->get('can_specify_domains'),
'can_share_tcp_ports' => (int) $request->get('can_share_tcp_ports'),
'max_connections' => (int) $request->get('max_connections'),
]; ];
$this->userRepository $this->userRepository

View File

@@ -3,13 +3,18 @@
namespace App\Server\Http\Controllers; namespace App\Server\Http\Controllers;
use App\Contracts\ConnectionManager; use App\Contracts\ConnectionManager;
use App\Contracts\DomainRepository;
use App\Contracts\SubdomainRepository;
use App\Contracts\UserRepository; use App\Contracts\UserRepository;
use App\Http\QueryParameters; use App\Http\QueryParameters;
use App\Server\Configuration;
use App\Server\Exceptions\NoFreePortAvailable;
use Illuminate\Support\Arr;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\WebSocket\MessageComponentInterface; use Ratchet\WebSocket\MessageComponentInterface;
use React\Promise\Deferred; use React\Promise\Deferred;
use React\Promise\FulfilledPromise;
use React\Promise\PromiseInterface; use React\Promise\PromiseInterface;
use function React\Promise\reject;
use stdClass; use stdClass;
class ControlMessageController implements MessageComponentInterface class ControlMessageController implements MessageComponentInterface
@@ -20,10 +25,22 @@ class ControlMessageController implements MessageComponentInterface
/** @var UserRepository */ /** @var UserRepository */
protected $userRepository; protected $userRepository;
public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository) /** @var SubdomainRepository */
protected $subdomainRepository;
/** @var DomainRepository */
protected $domainRepository;
/** @var Configuration */
protected $configuration;
public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository, SubdomainRepository $subdomainRepository, Configuration $configuration, DomainRepository $domainRepository)
{ {
$this->connectionManager = $connectionManager; $this->connectionManager = $connectionManager;
$this->userRepository = $userRepository; $this->userRepository = $userRepository;
$this->subdomainRepository = $subdomainRepository;
$this->domainRepository = $domainRepository;
$this->configuration = $configuration;
} }
/** /**
@@ -54,6 +71,10 @@ class ControlMessageController implements MessageComponentInterface
if (isset($connection->request_id)) { if (isset($connection->request_id)) {
return $this->sendResponseToHttpConnection($connection->request_id, $msg); return $this->sendResponseToHttpConnection($connection->request_id, $msg);
} }
if (isset($connection->tcp_request_id)) {
$connectionInfo = $this->connectionManager->findControlConnectionForClientId($connection->tcp_client_id);
$connectionInfo->proxyConnection->write($msg);
}
try { try {
$payload = json_decode($msg); $payload = json_decode($msg);
@@ -76,24 +97,48 @@ class ControlMessageController implements MessageComponentInterface
protected function authenticate(ConnectionInterface $connection, $data) protected function authenticate(ConnectionInterface $connection, $data)
{ {
if (! isset($data->subdomain)) {
$data->subdomain = null;
}
if (! isset($data->type)) {
$data->type = 'http';
}
if (! isset($data->server_host) || is_null($data->server_host)) {
$data->server_host = $this->configuration->hostname();
}
$this->verifyAuthToken($connection) $this->verifyAuthToken($connection)
->then(function () use ($connection, $data) { ->then(function ($user) use ($connection) {
if (! $this->hasValidSubdomain($connection, $data->subdomain)) { $maximumConnectionCount = config('expose.admin.maximum_open_connections_per_user', 0);
return;
if (is_null($user)) {
$connectionCount = count($this->connectionManager->findControlConnectionsForIp($connection->remoteAddress));
} else {
$maximumConnectionCount = Arr::get($user, 'max_connections', $maximumConnectionCount);
$connectionCount = count($this->connectionManager->findControlConnectionsForAuthToken($user['auth_token']));
} }
$connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $connection); if ($maximumConnectionCount > 0 && $connectionCount + 1 > $maximumConnectionCount) {
$connection->send(json_encode([
'event' => 'authenticationFailed',
'data' => [
'message' => config('expose.admin.messages.maximum_connection_count'),
],
]));
$connection->close();
$this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_connection_length')); reject(null);
}
$connection->send(json_encode([ return $user;
'event' => 'authenticated', })
'data' => [ ->then(function ($user) use ($connection, $data) {
'message' => config('expose.admin.messages.message_of_the_day'), if ($data->type === 'http') {
'subdomain' => $connectionInfo->subdomain, $this->handleHttpConnection($connection, $data, $user);
'client_id' => $connectionInfo->client_id, } elseif ($data->type === 'tcp') {
], $this->handleTcpConnection($connection, $data, $user);
])); }
}, function () use ($connection) { }, function () use ($connection) {
$connection->send(json_encode([ $connection->send(json_encode([
'event' => 'authenticationFailed', 'event' => 'authenticationFailed',
@@ -105,6 +150,91 @@ class ControlMessageController implements MessageComponentInterface
}); });
} }
protected function resolveConnectionMessage($connectionInfo, $user)
{
$deferred = new Deferred();
$connectionMessageResolver = config('expose.admin.messages.resolve_connection_message')($connectionInfo, $user);
if ($connectionMessageResolver instanceof PromiseInterface) {
$connectionMessageResolver->then(function ($connectionMessage) use ($connectionInfo, $deferred) {
$connectionInfo->message = $connectionMessage;
$deferred->resolve($connectionInfo);
});
} else {
$connectionInfo->message = $connectionMessageResolver;
return \React\Promise\resolve($connectionInfo);
}
return $deferred->promise();
}
protected function handleHttpConnection(ConnectionInterface $connection, $data, $user = null)
{
$this->hasValidDomain($connection, $data->server_host, $user)
->then(function () use ($connection, $data, $user) {
return $this->hasValidSubdomain($connection, $data->subdomain, $user, $data->server_host);
})
->then(function ($subdomain) use ($data, $connection, $user) {
if ($subdomain === false) {
return;
}
$data->subdomain = $subdomain;
$connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $data->server_host, $connection);
$this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_connection_length'));
return $this->resolveConnectionMessage($connectionInfo, $user);
})
->then(function ($connectionInfo) use ($connection, $user) {
$connection->send(json_encode([
'event' => 'authenticated',
'data' => [
'message' => $connectionInfo->message,
'subdomain' => $connectionInfo->subdomain,
'server_host' => $connectionInfo->serverHost,
'user' => $user,
'client_id' => $connectionInfo->client_id,
],
]));
});
}
protected function handleTcpConnection(ConnectionInterface $connection, $data, $user = null)
{
if (! $this->canShareTcpPorts($connection, $data, $user)) {
return;
}
try {
$connectionInfo = $this->connectionManager->storeTcpConnection($data->port, $connection);
} catch (NoFreePortAvailable $exception) {
$connection->send(json_encode([
'event' => 'authenticationFailed',
'data' => [
'message' => config('expose.admin.messages.no_free_tcp_port_available'),
],
]));
$connection->close();
return;
}
$connection->send(json_encode([
'event' => 'authenticated',
'data' => [
'message' => config('expose.admin.messages.resolve_connection_message')($connectionInfo, $user),
'user' => $user,
'port' => $connectionInfo->port,
'shared_port' => $connectionInfo->shared_port,
'client_id' => $connectionInfo->client_id,
],
]));
}
protected function registerProxy(ConnectionInterface $connection, $data) protected function registerProxy(ConnectionInterface $connection, $data)
{ {
$connection->request_id = $data->request_id; $connection->request_id = $data->request_id;
@@ -116,6 +246,18 @@ class ControlMessageController implements MessageComponentInterface
]); ]);
} }
protected function registerTcpProxy(ConnectionInterface $connection, $data)
{
$connection->tcp_client_id = $data->client_id;
$connection->tcp_request_id = $data->tcp_request_id;
$connectionInfo = $this->connectionManager->findControlConnectionForClientId($data->client_id);
$connectionInfo->emit('tcp_proxy_ready_'.$data->tcp_request_id, [
$connection,
]);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@@ -127,7 +269,7 @@ class ControlMessageController implements MessageComponentInterface
protected function verifyAuthToken(ConnectionInterface $connection): PromiseInterface protected function verifyAuthToken(ConnectionInterface $connection): PromiseInterface
{ {
if (config('expose.admin.validate_auth_tokens') !== true) { if (config('expose.admin.validate_auth_tokens') !== true) {
return new FulfilledPromise(); return \React\Promise\resolve(null);
} }
$deferred = new Deferred(); $deferred = new Deferred();
@@ -136,35 +278,145 @@ class ControlMessageController implements MessageComponentInterface
$this->userRepository $this->userRepository
->getUserByToken($authToken) ->getUserByToken($authToken)
->then(function ($user) use ($connection, $deferred) { ->then(function ($user) use ($deferred) {
if (is_null($user)) { if (is_null($user)) {
$deferred->reject(); $deferred->reject();
} else { } else {
$deferred->resolve($user); $this->userRepository
->updateLastSharedAt($user['id'])
->then(function () use ($deferred, $user) {
$deferred->resolve($user);
});
} }
}); });
return $deferred->promise(); return $deferred->promise();
} }
protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain): bool protected function hasValidDomain(ConnectionInterface $connection, ?string $serverHost, ?array $user): PromiseInterface
{ {
if (! is_null($user) && $serverHost !== $this->configuration->hostname()) {
$deferred = new Deferred();
$this->domainRepository
->getDomainsByUserId($user['id'])
->then(function ($domains) use ($connection, $deferred, $serverHost) {
$userDomain = collect($domains)->first(function ($domain) use ($serverHost) {
return strtolower($domain['domain']) === strtolower($serverHost);
});
if (is_null($userDomain)) {
$connection->send(json_encode([
'event' => 'authenticationFailed',
'data' => [
'message' => config('expose.admin.messages.custom_domain_unauthorized').PHP_EOL,
],
]));
$connection->close();
$deferred->reject(null);
return;
}
$deferred->resolve(null);
});
return $deferred->promise();
} else {
return \React\Promise\resolve(null);
}
}
protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain, ?array $user, string $serverHost): PromiseInterface
{
/**
* Check if the user can specify a custom subdomain in the first place.
*/
if (! is_null($user) && $user['can_specify_subdomains'] === 0 && ! is_null($subdomain)) {
$connection->send(json_encode([
'event' => 'error',
'data' => [
'message' => config('expose.admin.messages.custom_subdomain_unauthorized').PHP_EOL,
],
]));
return \React\Promise\resolve(null);
}
/**
* Check if the given subdomain is reserved for a different user.
*/
if (! is_null($subdomain)) { if (! is_null($subdomain)) {
$controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain); return $this->subdomainRepository->getSubdomainsByNameAndDomain($subdomain, $serverHost)
if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain')) { ->then(function ($foundSubdomains) use ($connection, $subdomain, $user, $serverHost) {
$message = config('expose.admin.messages.subdomain_taken'); $ownSubdomain = collect($foundSubdomains)->first(function ($subdomain) use ($user) {
$message = str_replace(':subdomain', $subdomain, $message); return $subdomain['user_id'] === $user['id'];
});
$connection->send(json_encode([ if (count($foundSubdomains) > 0 && ! is_null($user) && is_null($ownSubdomain)) {
'event' => 'subdomainTaken', $message = config('expose.admin.messages.subdomain_reserved', '');
'data' => [ $message = str_replace(':subdomain', $subdomain, $message);
'message' => $message,
],
]));
$connection->close();
return false; $connection->send(json_encode([
} 'event' => 'subdomainTaken',
'data' => [
'message' => $message,
],
]));
$connection->close();
return \React\Promise\resolve(false);
}
$controlConnection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost);
if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain') || in_array($subdomain, config('expose.admin.reserved_subdomains', []))) {
$message = config('expose.admin.messages.subdomain_taken');
$message = str_replace(':subdomain', $subdomain, $message);
$connection->send(json_encode([
'event' => 'subdomainTaken',
'data' => [
'message' => $message,
],
]));
$connection->close();
return \React\Promise\resolve(false);
}
return \React\Promise\resolve($subdomain);
});
}
return \React\Promise\resolve($subdomain);
}
protected function canShareTcpPorts(ConnectionInterface $connection, $data, $user)
{
if (! config('expose.admin.allow_tcp_port_sharing', true)) {
$connection->send(json_encode([
'event' => 'authenticationFailed',
'data' => [
'message' => config('expose.admin.messages.tcp_port_sharing_disabled'),
],
]));
$connection->close();
return false;
}
if (! is_null($user) && $user['can_share_tcp_ports'] === 0) {
$connection->send(json_encode([
'event' => 'authenticationFailed',
'data' => [
'message' => config('expose.admin.messages.tcp_port_sharing_unauthorized'),
],
]));
$connection->close();
return false;
} }
return true; return true;

View File

@@ -3,6 +3,7 @@
namespace App\Server\Http\Controllers; namespace App\Server\Http\Controllers;
use App\Contracts\ConnectionManager; use App\Contracts\ConnectionManager;
use App\Contracts\StatisticsCollector;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Server\Configuration; use App\Server\Configuration;
use App\Server\Connections\ControlConnection; use App\Server\Connections\ControlConnection;
@@ -27,15 +28,20 @@ class TunnelMessageController extends Controller
protected $modifiers = []; protected $modifiers = [];
public function __construct(ConnectionManager $connectionManager, Configuration $configuration) /** @var StatisticsCollector */
protected $statisticsCollector;
public function __construct(ConnectionManager $connectionManager, StatisticsCollector $statisticsCollector, Configuration $configuration)
{ {
$this->connectionManager = $connectionManager; $this->connectionManager = $connectionManager;
$this->configuration = $configuration; $this->configuration = $configuration;
$this->statisticsCollector = $statisticsCollector;
} }
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
{ {
$subdomain = $this->detectSubdomain($request); $subdomain = $this->detectSubdomain($request);
$serverHost = $this->detectServerHost($request);
if (is_null($subdomain)) { if (is_null($subdomain)) {
$httpConnection->send( $httpConnection->send(
@@ -46,7 +52,7 @@ class TunnelMessageController extends Controller
return; return;
} }
$controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain); $controlConnection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost);
if (is_null($controlConnection)) { if (is_null($controlConnection)) {
$httpConnection->send( $httpConnection->send(
@@ -57,14 +63,23 @@ class TunnelMessageController extends Controller
return; return;
} }
$this->statisticsCollector->incomingRequest();
$this->sendRequestToClient($request, $controlConnection, $httpConnection); $this->sendRequestToClient($request, $controlConnection, $httpConnection);
} }
protected function detectSubdomain(Request $request): ?string protected function detectSubdomain(Request $request): ?string
{ {
$subdomain = Str::before($request->getHost(), '.'.$this->configuration->hostname()); $serverHost = $this->detectServerHost($request);
return $subdomain === $request->getHost() ? null : $subdomain; $subdomain = Str::before($request->header('Host'), '.'.$serverHost);
return $subdomain === $request->header('Host') ? null : $subdomain;
}
protected function detectServerHost(Request $request): ?string
{
return Str::before(Str::after($request->header('Host'), '.'), ':');
} }
protected function sendRequestToClient(Request $request, ControlConnection $controlConnection, ConnectionInterface $httpConnection) protected function sendRequestToClient(Request $request, ControlConnection $controlConnection, ConnectionInterface $httpConnection)
@@ -75,7 +90,7 @@ class TunnelMessageController extends Controller
$httpConnection = $this->connectionManager->storeHttpConnection($httpConnection, $requestId); $httpConnection = $this->connectionManager->storeHttpConnection($httpConnection, $requestId);
transform($this->passRequestThroughModifiers($request, $httpConnection), function (Request $request) use ($controlConnection, $httpConnection, $requestId) { transform($this->passRequestThroughModifiers($request, $httpConnection), function (Request $request) use ($controlConnection, $requestId) {
$controlConnection->once('proxy_ready_'.$requestId, function (ConnectionInterface $proxy) use ($request) { $controlConnection->once('proxy_ready_'.$requestId, function (ConnectionInterface $proxy) use ($request) {
// Convert the Laravel request into a PSR7 request // Convert the Laravel request into a PSR7 request
$psr17Factory = new Psr17Factory(); $psr17Factory = new Psr17Factory();
@@ -107,7 +122,7 @@ class TunnelMessageController extends Controller
{ {
$request::setTrustedProxies([$controlConnection->socket->remoteAddress, '127.0.0.1'], Request::HEADER_X_FORWARDED_ALL); $request::setTrustedProxies([$controlConnection->socket->remoteAddress, '127.0.0.1'], Request::HEADER_X_FORWARDED_ALL);
$host = $this->configuration->hostname(); $host = $controlConnection->serverHost;
if (! $request->isSecure()) { if (! $request->isSecure()) {
$host .= ":{$this->configuration->port()}"; $host .= ":{$this->configuration->port()}";
@@ -119,6 +134,7 @@ class TunnelMessageController extends Controller
$request->headers->set('Upgrade-Insecure-Requests', 1); $request->headers->set('Upgrade-Insecure-Requests', 1);
$request->headers->set('X-Exposed-By', config('app.name').' '.config('app.version')); $request->headers->set('X-Exposed-By', config('app.name').' '.config('app.version'));
$request->headers->set('X-Original-Host', "{$controlConnection->subdomain}.{$host}"); $request->headers->set('X-Original-Host', "{$controlConnection->subdomain}.{$host}");
$request->headers->set('X-Forwarded-Host', "{$controlConnection->subdomain}.{$host}");
return $request; return $request;
} }

View File

@@ -35,6 +35,7 @@ class Router implements HttpServerInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*
* @throws \UnexpectedValueException If a controller is not \Ratchet\Http\HttpServerInterface * @throws \UnexpectedValueException If a controller is not \Ratchet\Http\HttpServerInterface
*/ */
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) public function onOpen(ConnectionInterface $conn, RequestInterface $request = null)

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Server\LoggerRepository;
use App\Contracts\LoggerRepository;
use App\Contracts\UserRepository;
use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
class DatabaseLogger implements LoggerRepository
{
/** @var DatabaseInterface */
protected $database;
public function __construct(DatabaseInterface $database)
{
$this->database = $database;
}
public function logSubdomain($authToken, $subdomain)
{
app(UserRepository::class)->getUserByToken($authToken)
->then(function ($user) use ($subdomain) {
$this->database->query("
INSERT INTO logs (user_id, subdomain, created_at)
VALUES (:user_id, :subdomain, DATETIME('now'))
", [
'user_id' => $user['id'],
'subdomain' => $subdomain,
])->then(function () {
$this->cleanOldLogs();
});
});
}
public function cleanOldLogs()
{
$this->database->query("DELETE FROM logs WHERE created_at < date('now', '-30 day')");
}
public function getLogsBySubdomain($subdomain): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('
SELECT
logs.id AS log_id,
logs.subdomain,
users.*
FROM logs
INNER JOIN users
ON users.id = logs.user_id
WHERE logs.subdomain = :subdomain', ['subdomain' => $subdomain])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows);
});
return $deferred->promise();
}
public function getLogs(): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('
SELECT
logs.id AS log_id,
logs.subdomain,
users.*
FROM logs
INNER JOIN users
ON users.id = logs.user_id')
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows);
});
return $deferred->promise();
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Server\LoggerRepository;
use App\Contracts\LoggerRepository;
use React\Promise\PromiseInterface;
class NullLogger implements LoggerRepository
{
public function logSubdomain($authToken, $subdomain)
{
// noop
}
public function getLogsBySubdomain($subdomain): PromiseInterface
{
return \React\Promise\resolve([]);
}
public function getLogs(): PromiseInterface
{
return \React\Promise\resolve([]);
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace App\Server\StatisticsCollector;
use App\Contracts\StatisticsCollector;
use Clue\React\SQLite\DatabaseInterface;
class DatabaseStatisticsCollector implements StatisticsCollector
{
/** @var DatabaseInterface */
protected $database;
/** @var array */
protected $sharedPorts = [];
/** @var array */
protected $sharedSites = [];
/** @var int */
protected $requests = 0;
public function __construct(DatabaseInterface $database)
{
$this->database = $database;
}
/**
* Flush the stored statistics.
*
* @return void
*/
public function flush()
{
$this->sharedPorts = [];
$this->sharedSites = [];
$this->requests = 0;
}
public function siteShared($authToken = null)
{
if (! $this->shouldCollectStatistics()) {
return;
}
if (! isset($this->sharedSites[$authToken])) {
$this->sharedSites[$authToken] = 0;
}
$this->sharedSites[$authToken]++;
}
public function portShared($authToken = null)
{
if (! $this->shouldCollectStatistics()) {
return;
}
if (! isset($this->sharedPorts[$authToken])) {
$this->sharedPorts[$authToken] = 0;
}
$this->sharedPorts[$authToken]++;
}
public function incomingRequest()
{
if (! $this->shouldCollectStatistics()) {
return;
}
$this->requests++;
}
public function save()
{
$sharedSites = 0;
collect($this->sharedSites)->map(function ($numSites) use (&$sharedSites) {
$sharedSites += $numSites;
});
$sharedPorts = 0;
collect($this->sharedPorts)->map(function ($numPorts) use (&$sharedPorts) {
$sharedPorts += $numPorts;
});
$this->database->query('
INSERT INTO statistics (timestamp, shared_sites, shared_ports, unique_shared_sites, unique_shared_ports, incoming_requests)
VALUES (:timestamp, :shared_sites, :shared_ports, :unique_shared_sites, :unique_shared_ports, :incoming_requests)
', [
'timestamp' => today()->toDateString(),
'shared_sites' => $sharedSites,
'shared_ports' => $sharedPorts,
'unique_shared_sites' => count($this->sharedSites),
'unique_shared_ports' => count($this->sharedPorts),
'incoming_requests' => $this->requests,
])
->then(function () {
$this->flush();
});
}
public function shouldCollectStatistics(): bool
{
return config('expose.admin.statistics.enable_statistics', true);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Server\StatisticsRepository;
use App\Contracts\StatisticsRepository;
use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
class DatabaseStatisticsRepository implements StatisticsRepository
{
/** @var DatabaseInterface */
protected $database;
public function __construct(DatabaseInterface $database)
{
$this->database = $database;
}
public function getStatistics($from, $until): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT
timestamp,
SUM(shared_sites) as shared_sites,
SUM(shared_ports) as shared_ports,
SUM(unique_shared_sites) as unique_shared_sites,
SUM(unique_shared_ports) as unique_shared_ports,
SUM(incoming_requests) as incoming_requests
FROM statistics
WHERE
`timestamp` >= "'.$from.'" AND `timestamp` <= "'.$until.'"')
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows);
});
return $deferred->promise();
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace App\Server\SubdomainRepository;
use App\Contracts\SubdomainRepository;
use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
class DatabaseSubdomainRepository implements SubdomainRepository
{
/** @var DatabaseInterface */
protected $database;
public function __construct(DatabaseInterface $database)
{
$this->database = $database;
}
public function getSubdomains(): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM subdomains ORDER by created_at DESC')
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows);
});
return $deferred->promise();
}
public function getSubdomainById($id): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $id])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0] ?? null);
});
return $deferred->promise();
}
public function getSubdomainByName(string $name): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM subdomains WHERE subdomain = :name', ['name' => $name])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0] ?? null);
});
return $deferred->promise();
}
public function getSubdomainByNameAndDomain(string $name, string $domain): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM subdomains WHERE subdomain = :name AND domain = :domain', [
'name' => $name,
'domain' => $domain,
])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0] ?? null);
});
return $deferred->promise();
}
public function getSubdomainsByNameAndDomain(string $name, string $domain): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM subdomains WHERE subdomain = :name AND domain = :domain', [
'name' => $name,
'domain' => $domain,
])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows);
});
return $deferred->promise();
}
public function getSubdomainsByUserId($id): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM subdomains WHERE user_id = :user_id ORDER by created_at DESC', [
'user_id' => $id,
])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows);
});
return $deferred->promise();
}
public function storeSubdomain(array $data): PromiseInterface
{
$deferred = new Deferred();
$this->database->query("
INSERT INTO subdomains (user_id, subdomain, domain, created_at)
VALUES (:user_id, :subdomain, :domain, DATETIME('now'))
", $data)
->then(function (Result $result) use ($deferred) {
$this->database->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $result->insertId])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0]);
});
});
return $deferred->promise();
}
public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT * FROM subdomains WHERE user_id = :user_id AND subdomain = :name ORDER by created_at DESC', [
'user_id' => $id,
'name' => $name,
])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows);
});
return $deferred->promise();
}
public function deleteSubdomainForUserId($userId, $subdomainId): PromiseInterface
{
$deferred = new Deferred();
$this->database->query('DELETE FROM subdomains WHERE (id = :id OR subdomain = :id) AND user_id = :user_id', [
'id' => $subdomainId,
'user_id' => $userId,
])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result);
});
return $deferred->promise();
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Server\Support;
use App\Server\Connections\ControlConnection;
use Clue\React\Buzz\Browser;
use Psr\Http\Message\ResponseInterface;
class RetrieveWelcomeMessageFromApi
{
/** @var Browser */
protected $browser;
/** @var string */
protected $url;
public function __construct(Browser $browser)
{
$this->browser = $browser;
$this->url = config('expose.admin.welcome_message_api_url');
}
public function forUser(ControlConnection $connectionInfo, $user)
{
return $this->browser
->post($this->url, [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
], json_encode([
'user' => $user,
'connectionInfo' => $connectionInfo->toArray(),
]))
->then(function (ResponseInterface $response) {
$result = json_decode($response->getBody());
return $result->message ?? '';
}, function (Exception $e) {
return '';
});
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Server\UserRepository; namespace App\Server\UserRepository;
use App\Contracts\ConnectionManager;
use App\Contracts\UserRepository; use App\Contracts\UserRepository;
use Clue\React\SQLite\DatabaseInterface; use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result; use Clue\React\SQLite\Result;
@@ -13,9 +14,13 @@ class DatabaseUserRepository implements UserRepository
/** @var DatabaseInterface */ /** @var DatabaseInterface */
protected $database; protected $database;
public function __construct(DatabaseInterface $database) /** @var ConnectionManager */
protected $connectionManager;
public function __construct(DatabaseInterface $database, ConnectionManager $connectionManager)
{ {
$this->database = $database; $this->database = $database;
$this->connectionManager = $connectionManager;
} }
public function getUsers(): PromiseInterface public function getUsers(): PromiseInterface
@@ -31,6 +36,65 @@ class DatabaseUserRepository implements UserRepository
return $deferred->promise(); return $deferred->promise();
} }
public function paginateUsers(string $searchQuery, int $perPage, int $currentPage): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT COUNT(*) AS count FROM users')
->then(function (Result $result) use ($searchQuery, $deferred, $perPage, $currentPage) {
$totalUsers = $result->rows[0]['count'];
$query = 'SELECT * FROM users ';
$bindings = [
'limit' => $perPage + 1,
'offset' => $currentPage < 2 ? 0 : ($currentPage - 1) * $perPage,
];
if ($searchQuery !== '') {
$query .= "WHERE name LIKE '%".$searchQuery."%' ";
$bindings['search'] = $searchQuery;
}
$query .= ' ORDER by created_at DESC LIMIT :limit OFFSET :offset';
$this->database
->query($query, $bindings)
->then(function (Result $result) use ($deferred, $perPage, $currentPage, $totalUsers) {
if (count($result->rows) == $perPage + 1) {
array_pop($result->rows);
$nextPage = $currentPage + 1;
}
$users = collect($result->rows)->map(function ($user) {
return $this->getUserDetails($user);
})->toArray();
$paginated = [
'total' => $totalUsers,
'users' => $users,
'current_page' => $currentPage,
'per_page' => $perPage,
'next_page' => $nextPage ?? null,
'previous_page' => $currentPage > 1 ? $currentPage - 1 : null,
];
$deferred->resolve($paginated);
});
});
return $deferred->promise();
}
protected function getUserDetails(array $user)
{
$user['sites'] = $user['auth_token'] !== '' ? $this->connectionManager->getConnectionsForAuthToken($user['auth_token']) : [];
$user['tcp_connections'] = $user['auth_token'] !== '' ? $this->connectionManager->getTcpConnectionsForAuthToken($user['auth_token']) : [];
return $user;
}
public function getUserById($id): PromiseInterface public function getUserById($id): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
@@ -38,7 +102,26 @@ class DatabaseUserRepository implements UserRepository
$this->database $this->database
->query('SELECT * FROM users WHERE id = :id', ['id' => $id]) ->query('SELECT * FROM users WHERE id = :id', ['id' => $id])
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0] ?? null); $user = $result->rows[0] ?? null;
if (! is_null($user)) {
$user = $this->getUserDetails($user);
}
$deferred->resolve($user);
});
return $deferred->promise();
}
public function updateLastSharedAt($id): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query("UPDATE users SET last_shared_at = date('now') WHERE id = :id", ['id' => $id])
->then(function (Result $result) use ($deferred) {
$deferred->resolve();
}); });
return $deferred->promise(); return $deferred->promise();
@@ -51,7 +134,13 @@ class DatabaseUserRepository implements UserRepository
$this->database $this->database
->query('SELECT * FROM users WHERE auth_token = :token', ['token' => $authToken]) ->query('SELECT * FROM users WHERE auth_token = :token', ['token' => $authToken])
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0] ?? null); $user = $result->rows[0] ?? null;
if (! is_null($user)) {
$user = $this->getUserDetails($user);
}
$deferred->resolve($user);
}); });
return $deferred->promise(); return $deferred->promise();
@@ -61,15 +150,38 @@ class DatabaseUserRepository implements UserRepository
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->database->query(" $this->getUserByToken($data['auth_token'])
INSERT INTO users (name, auth_token, created_at) ->then(function ($existingUser) use ($data, $deferred) {
VALUES (:name, :auth_token, DATETIME('now')) if (is_null($existingUser)) {
$this->database->query("
INSERT INTO users (name, auth_token, can_specify_subdomains, can_specify_domains, can_share_tcp_ports, max_connections, created_at)
VALUES (:name, :auth_token, :can_specify_subdomains, :can_specify_domains, :can_share_tcp_ports, :max_connections, DATETIME('now'))
", $data) ", $data)
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
$this->database->query('SELECT * FROM users WHERE id = :id', ['id' => $result->insertId]) $this->database->query('SELECT * FROM users WHERE id = :id', ['id' => $result->insertId])
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0]); $deferred->resolve($result->rows[0]);
}); });
});
} else {
$this->database->query('
UPDATE users
SET
name = :name,
can_specify_subdomains = :can_specify_subdomains,
can_specify_domains = :can_specify_domains,
can_share_tcp_ports = :can_share_tcp_ports,
max_connections = :max_connections
WHERE
auth_token = :auth_token
', $data)
->then(function (Result $result) use ($existingUser, $deferred) {
$this->database->query('SELECT * FROM users WHERE id = :id', ['id' => $existingUser['id']])
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0]);
});
});
}
}); });
return $deferred->promise(); return $deferred->promise();
@@ -79,11 +191,31 @@ class DatabaseUserRepository implements UserRepository
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->database->query('DELETE FROM users WHERE id = :id', ['id' => $id]) $this->database->query('DELETE FROM users WHERE id = :id OR auth_token = :id', ['id' => $id])
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
$deferred->resolve($result); $deferred->resolve($result);
}); });
return $deferred->promise(); return $deferred->promise();
} }
public function getUsersByTokens(array $authTokens): PromiseInterface
{
$deferred = new Deferred();
$authTokenString = collect($authTokens)->map(function ($token) {
return '"'.$token.'"';
})->join(',');
$this->database->query('SELECT * FROM users WHERE auth_token IN ('.$authTokenString.')')
->then(function (Result $result) use ($deferred) {
$users = collect($result->rows)->map(function ($user) {
return $this->getUserDetails($user);
})->toArray();
$deferred->resolve($users);
});
return $deferred->promise();
}
} }

View File

@@ -1,11 +1,11 @@
<?php <?php
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use function GuzzleHttp\Psr7\str;
function respond_json($responseData, int $statusCode = 200) function respond_json($responseData, int $statusCode = 200)
{ {
return str(new Response( return Message::toString(new Response(
$statusCode, $statusCode,
['Content-Type' => 'application/json'], ['Content-Type' => 'application/json'],
json_encode($responseData, JSON_INVALID_UTF8_IGNORE) json_encode($responseData, JSON_INVALID_UTF8_IGNORE)
@@ -14,7 +14,7 @@ function respond_json($responseData, int $statusCode = 200)
function respond_html(string $html, int $statusCode = 200) function respond_html(string $html, int $statusCode = 200)
{ {
return str(new Response( return Message::toString(new Response(
$statusCode, $statusCode,
['Content-Type' => 'text/html'], ['Content-Type' => 'text/html'],
$html $html

2
builds/.gitignore vendored
View File

@@ -1,2 +0,0 @@
!.gitignore
*

Binary file not shown.

View File

@@ -1,53 +1,61 @@
{ {
"name": "beyondcode/expose", "name": "bitinflow/expose",
"description": "Expose",
"keywords": ["expose", "tunnel", "ngrok"],
"homepage": "https://sharedwithexpose.com",
"type": "project", "type": "project",
"description": "Create public URLs for local sites through any firewall and VPN.",
"keywords": [
"expose",
"tunnel",
"ngrok"
],
"homepage": "https://bitinflow.dev",
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{
"name": "René Preuß",
"email": "rene@bitinflow.com"
},
{ {
"name": "Marcel Pociot", "name": "Marcel Pociot",
"email": "marcel@beyondco.de" "email": "marcel@beyondco.de"
} }
], ],
"repositories": [
{
"type": "vcs",
"url": "https://github.com/seankndy/reactphp-sqlite"
}
],
"require": { "require": {
"php": "^7.3.0", "php": "^8.0",
"ext-json": "*", "ext-json": "*",
"padraic/phar-updater": "^1.0.6" "laravel-zero/phar-updater": "^1.2"
}, },
"require-dev": { "require-dev": {
"nikic/php-parser": "^4.4", "cboden/ratchet": "^0.4.3",
"cboden/ratchet": "^0.4.2", "clue/block-react": "^1.4",
"clue/block-react": "^1.3", "clue/buzz-react": "^2.9",
"clue/buzz-react": "^2.7",
"clue/reactphp-sqlite": "dev-modular-worker-for-phar-support", "clue/reactphp-sqlite": "dev-modular-worker-for-phar-support",
"guzzlehttp/guzzle": "^6.5", "guzzlehttp/guzzle": "^7.2",
"guzzlehttp/psr7": "dev-master as 1.6.1", "guzzlehttp/psr7": "^1.7",
"illuminate/http": "5.8.*|^6.0|^7.0", "illuminate/log": "^8.0",
"illuminate/pipeline": "^7.6", "illuminate/http": "5.8.* || ^6.0 || ^7.0 || ^8.0",
"illuminate/validation": "^7.7", "illuminate/pipeline": "^7.6 || ^8.0",
"laminas/laminas-http": "^2.11", "illuminate/validation": "^7.7 || ^8.0",
"laravel-zero/framework": "^7.0", "laminas/laminas-http": "^2.13",
"mockery/mockery": "^1.3", "laravel-zero/framework": "^8.2",
"namshi/cuzzle": "^2.0", "mockery/mockery": "^1.4.2",
"nyholm/psr7": "^1.2", "octoper/cuzzle": "^3.1",
"phpunit/phpunit": "^8.5", "nikic/php-parser": "^v4.10",
"ratchet/pawl": "^0.3.4", "nyholm/psr7": "^1.3",
"react/http": "^0.8.6", "phpunit/phpunit": "^9.4.3",
"ratchet/pawl": "^0.3.5",
"react/http": "^1.1.0",
"react/socket": "^1.6",
"react/stream": "^1.1.1", "react/stream": "^1.1.1",
"react/socket": "^1.4",
"riverline/multipart-parser": "^2.0", "riverline/multipart-parser": "^2.0",
"symfony/expression-language": "^5.0", "symfony/expression-language": "^5.2",
"symfony/http-kernel": "^4.0|^5.0", "symfony/http-kernel": "^4.0 || ^5.2",
"symfony/psr-http-message-bridge": "^2.0", "symfony/psr-http-message-bridge": "^2.0",
"twig/twig": "^3.0" "twig/twig": "^3.1"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@@ -62,17 +70,20 @@
"Tests\\": "tests/" "Tests\\": "tests/"
} }
}, },
"config": { "repositories": [
"preferred-install": "dist", {
"sort-packages": true, "type": "vcs",
"optimize-autoloader": true "url": "https://github.com/seankndy/reactphp-sqlite"
}, }
],
"minimum-stability": "dev",
"prefer-stable": true,
"bin": [
"builds/expose"
],
"scripts": { "scripts": {
"post-create-project-cmd": [ "post-create-project-cmd": [
"@php application app:rename" "@php application app:rename"
] ]
}, }
"minimum-stability": "dev",
"prefer-stable": true,
"bin": ["builds/expose"]
} }

8984
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ return [
| |
*/ */
'version' => app('git.version'), 'version' => '2.2.2',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -59,4 +59,6 @@ return [
Illuminate\Translation\TranslationServiceProvider::class, Illuminate\Translation\TranslationServiceProvider::class,
], ],
'locale' => 'en',
]; ];

View File

@@ -13,7 +13,7 @@ return [
| |
*/ */
'default' => \App\Commands\ShareCurrentWorkingDirectoryCommand::class, 'default' => NunoMaduro\LaravelConsoleSummary\SummaryCommand::class,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@@ -4,30 +4,58 @@ return [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Host | Servers
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| The expose server to connect to. By default, expose is using the free | The available Expose servers that your client can connect to.
| expose.dev server, offered by Beyond Code. You will need a free | When sharing sites or TCP ports, you can specify the server
| Beyond Code account in order to authenticate with the server. | that should be used using the `--server=` option.
| Feel free to host your own server and change this value.
| |
*/ */
'host' => 'sharedwithexpose.com', 'servers' => [
'free' => [
'host' => 'bitinflow.dev',
'port' => 443,
],
],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Port | Server Endpoint
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| The port that expose will try to connect to. If you want to bypass | When you specify a server that does not exist in above static array,
| firewalls and have proper SSL encrypted tunnels, make sure to use | Expose will perform a GET request to this URL and tries to retrieve
| port 443 and use a reverse proxy for Expose. | a JSON payload that looks like the configurations servers array.
| |
| The free default server is already running on port 443. | Expose then tries to load the configuration for the given server
| if available.
| |
*/ */
'port' => 443, 'server_endpoint' => 'https://bitinflow.dev/api/servers',
/*
|--------------------------------------------------------------------------
| Default Server
|--------------------------------------------------------------------------
|
| The default server from the servers array,
| or the servers endpoint above.
|
*/
'default_server' => 'free',
/*
|--------------------------------------------------------------------------
| DNS
|--------------------------------------------------------------------------
|
| The DNS server to use when resolving the shared URLs.
| When Expose is running from within Docker containers, you should set this to
| `true` to fall-back to the system default DNS servers.
|
*/
'dns' => '127.0.0.1',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -43,6 +71,20 @@ return [
*/ */
'auth_token' => '', 'auth_token' => '',
/*
|--------------------------------------------------------------------------
| Default Domain
|--------------------------------------------------------------------------
|
| The custom domain to use when sharing sites with Expose.
| You can register your own custom domain using Expose Pro
| Learn more at: https://expose.dev/get-pro
|
| > expose default-domain YOUR-CUSTOM-WHITELABEL-DOMAIN
|
*/
'default_domain' => null,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Default TLD | Default TLD
@@ -55,6 +97,18 @@ return [
*/ */
'default_tld' => 'test', 'default_tld' => 'test',
/*
|--------------------------------------------------------------------------
| Default HTTPS
|--------------------------------------------------------------------------
|
| Whether to use HTTPS as a default when sharing your local sites. Expose
| will try to look up the protocol if you are using Laravel Valet
| automatically. Otherwise you can specify it here manually.
|
*/
'default_https' => false,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Maximum Logged Requests | Maximum Logged Requests
@@ -78,7 +132,7 @@ return [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Maximum Allowed Memory | Skip Response Logging
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Sometimes, some responses don't need to be logged. Some are too big, | Sometimes, some responses don't need to be logged. Some are too big,
@@ -151,6 +205,37 @@ return [
*/ */
'validate_auth_tokens' => false, 'validate_auth_tokens' => false,
/*
|--------------------------------------------------------------------------
| TCP Port Sharing
|--------------------------------------------------------------------------
|
| Control if you want to allow users to share TCP ports with your Expose
| server. You can add fine-grained control per authentication token,
| but if you want to disable TCP port sharing in general, set this
| value to false.
|
*/
'allow_tcp_port_sharing' => true,
/*
|--------------------------------------------------------------------------
| TCP Port Range
|--------------------------------------------------------------------------
|
| Expose allows you to also share TCP ports, for example when sharing your
| local SSH server with the public. This setting allows you to define the
| port range that Expose will use to assign new ports to the users.
|
| Note: Do not use port ranges below 1024, as it might require root
| privileges to assign these ports.
|
*/
'tcp_port_range' => [
'from' => 50000,
'to' => 60000,
],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Maximum connection length | Maximum connection length
@@ -164,6 +249,21 @@ return [
*/ */
'maximum_connection_length' => 0, 'maximum_connection_length' => 0,
/*
|--------------------------------------------------------------------------
| Maximum number of open connections
|--------------------------------------------------------------------------
|
| You can limit the amount of connections that one client/user can have
| open. A maximum connection count of 0 means that clients can open
| as many connections as they want.
|
| When creating users with the API/admin interface, you can
| override this setting per user.
|
*/
'maximum_open_connections_per_user' => 0,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Subdomain | Subdomain
@@ -176,6 +276,17 @@ return [
*/ */
'subdomain' => 'expose', 'subdomain' => 'expose',
/*
|--------------------------------------------------------------------------
| Reserved Subdomain
|--------------------------------------------------------------------------
|
| Specify any subdomains that you don't want to be able to register
| on your expose server.
|
*/
'reserved_subdomains' => [],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Subdomain Generator | Subdomain Generator
@@ -188,6 +299,25 @@ return [
*/ */
'subdomain_generator' => \App\Server\SubdomainGenerator\RandomSubdomainGenerator::class, 'subdomain_generator' => \App\Server\SubdomainGenerator\RandomSubdomainGenerator::class,
/*
|--------------------------------------------------------------------------
| Connection Callback
|--------------------------------------------------------------------------
|
| This is a callback method that will be called when a new connection is
| established.
| The \App\Client\Callbacks\WebHookConnectionCallback::class is included out of the box.
|
*/
'connection_callback' => null,
'connection_callbacks' => [
'webhook' => [
'url' => null,
'secret' => null,
],
],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Users | Users
@@ -214,6 +344,10 @@ return [
*/ */
'user_repository' => \App\Server\UserRepository\DatabaseUserRepository::class, 'user_repository' => \App\Server\UserRepository\DatabaseUserRepository::class,
'subdomain_repository' => \App\Server\SubdomainRepository\DatabaseSubdomainRepository::class,
'logger_repository' => \App\Server\LoggerRepository\NullLogger::class,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Messages | Messages
@@ -225,11 +359,35 @@ return [
| |
*/ */
'messages' => [ 'messages' => [
'resolve_connection_message' => function ($connectionInfo, $user) {
return config('expose.admin.messages.message_of_the_day');
},
'message_of_the_day' => 'Thank you for using expose.', 'message_of_the_day' => 'Thank you for using expose.',
'invalid_auth_token' => 'Authentication failed. Please check your authentication token and try again.', 'invalid_auth_token' => 'Authentication failed. Please check your authentication token and try again.',
'subdomain_taken' => 'The chosen subdomain :subdomain is already taken. Please choose a different subdomain.', 'subdomain_taken' => 'The chosen subdomain :subdomain is already taken. Please choose a different subdomain.',
'subdomain_reserved' => 'The chosen subdomain :subdomain is not available. Please choose a different subdomain.',
'custom_subdomain_unauthorized' => 'You are not allowed to specify custom subdomains. Please upgrade to Expose Pro. Assigning a random subdomain instead.',
'custom_domain_unauthorized' => 'You are not allowed to use this custom domain.',
'tcp_port_sharing_unauthorized' => 'You are not allowed to share TCP ports. Please upgrade to Expose Pro.',
'no_free_tcp_port_available' => 'There are no free TCP ports available on this server. Please try again later.',
'tcp_port_sharing_disabled' => 'TCP port sharing is not available on this Expose server.',
],
'statistics' => [
'enable_statistics' => true,
'interval_in_seconds' => 3600,
'repository' => \App\Server\StatisticsRepository\DatabaseStatisticsRepository::class,
], ],
], ],
]; ];

113
config/logging.php Normal file
View File

@@ -0,0 +1,113 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that gets used when writing
| messages to the logs. The name specified in this option should match
| one of the channels defined in the "channels" configuration array.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Out of
| the box, Laravel uses the Monolog PHP logging library. This gives
| you a variety of powerful log handlers / formatters to utilize.
|
| Available Drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog",
| "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['stderr'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
'days' => 14,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => 'critical',
],
'papertrail' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => SyslogUdpHandler::class,
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
],
],
'stderr' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stderr',
],
],
'deprecations' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stderr',
],
],
'syslog' => [
'driver' => 'syslog',
'level' => 'debug',
],
'errorlog' => [
'driver' => 'errorlog',
'level' => 'debug',
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

Some files were not shown because too many files have changed in this diff Show More