210 Commits

Author SHA1 Message Date
René Preuß
91b0b98e85 Update bin 2022-07-07 22:32:53 +02:00
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
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
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
136 changed files with 4535 additions and 9231 deletions

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

View File

@@ -1,4 +1,4 @@
FROM php:7.4-cli FROM php:8.0-cli
RUN apt-get update RUN apt-get update
RUN apt-get install -y git libzip-dev zip RUN apt-get install -y git libzip-dev zip
@@ -20,5 +20,6 @@ ENV username=username
ENV password=password ENV password=password
ENV exposeConfigPath=/src/config/expose.php ENV exposeConfigPath=/src/config/expose.php
CMD sed -i "s|username|${username}|g" ${exposeConfigPath} && sed -i "s|password|${password}|g" ${exposeConfigPath} && php expose serve ${domain} --port ${port} --validateAuthTokens COPY docker-entrypoint.sh /usr/bin/
ENTRYPOINT ["/src/expose"] 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,18 +44,23 @@ class Client
$this->logger = $logger; $this->logger = $logger;
} }
public function share(string $sharedUrl, array $subdomains = [], string $hostname = '') 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, $hostname, config('expose.auth_token')); $this->connectToServer($sharedUrl, $subdomain, $serverHost, $this->configuration->auth());
} }
} }
public function sharePort(int $port) public function sharePort(int $port)
{ {
$this->connectToServerAndShareTcp($port, config('expose.auth_token')); $this->connectToServerAndShareTcp($port, $this->configuration->auth());
} }
protected function prepareSharedUrl(string $sharedUrl): string protected function prepareSharedUrl(string $sharedUrl): string
@@ -72,28 +81,30 @@ class Client
return $url; return $url;
} }
public function connectToServer(string $sharedUrl, $subdomain, $hostname = '', $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, $hostname, $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, $hostname); $connection->authenticate($sharedUrl, $subdomain, $serverHost);
$clientConnection->on('close', function () use ($sharedUrl, $subdomain, $hostname, $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(function () use ($sharedUrl, $subdomain, $hostname, $authToken) { $this->retryConnectionOrExit(function () use ($sharedUrl, $subdomain, $serverHost, $authToken) {
$this->connectToServer($sharedUrl, $subdomain, $hostname, $authToken); $this->connectToServer($sharedUrl, $subdomain, $serverHost, $authToken);
}); });
}); });
@@ -107,25 +118,22 @@ class Client
$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()}";
}
if ($data->hostname !== '' && ! is_null($data->hostname)) { $host = $data->server_host ?? $this->configuration->host();
$exposeUrl = "{$httpProtocol}://{$data->hostname}";
} else { $this->configuration->setServerHost($host);
$exposeUrl = "{$httpProtocol}://{$data->subdomain}.{$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{$exposeUrl}"); $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);
}); });
@@ -152,8 +160,9 @@ class Client
$promise = $deferred->promise(); $promise = $deferred->promise();
$wsProtocol = $this->configuration->port() === 443 ? 'wss' : 'ws'; $wsProtocol = $this->configuration->port() === 443 ? 'wss' : 'ws';
$exposeVersion = config('app.version');
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 ($port, $deferred, $authToken) { ->then(function (WebSocket $clientConnection) use ($port, $deferred, $authToken) {
@@ -177,9 +186,9 @@ class Client
$host = $this->configuration->host(); $host = $this->configuration->host();
$this->logger->info($data->message); $this->logger->info($data->message);
$this->logger->info("Local-Port:\t\t{$port}"); $this->logger->info("Local-Port:\t\t<options=bold>{$port}</>");
$this->logger->info("Shared-Port:\t\t{$data->shared_port}"); $this->logger->info("Shared-Port:\t\t<options=bold>{$data->shared_port}</>");
$this->logger->info("Expose-URL:\t\ttcp://{$host}:{$data->shared_port}."); $this->logger->info("Expose-URL:\t\t<options=bold>tcp://{$host}:{$data->shared_port}</>");
$this->logger->line(''); $this->logger->line('');
$deferred->resolve($data); $deferred->resolve($data);
@@ -207,6 +216,10 @@ class Client
$this->logger->info($data->message); $this->logger->info($data->message);
}); });
$connection->on('warning', function ($data) {
$this->logger->warn($data->message);
});
$connection->on('error', function ($data) { $connection->on('error', function ($data) {
$this->logger->error($data->message); $this->logger->error($data->message);
}); });
@@ -237,7 +250,9 @@ class Client
$deferred->reject(); $deferred->reject();
$this->loop->futureTick(function () { $this->loop->futureTick(function () {
exit(1); if ($this->shouldExit) {
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,11 +35,26 @@ 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);
@@ -40,7 +63,7 @@ class Configuration
public function getUrl(string $subdomain): string public function getUrl(string $subdomain): string
{ {
$httpProtocol = $this->port() === 443 ? 'https' : 'http'; $httpProtocol = $this->port() === 443 ? 'https' : 'http';
$host = $this->host(); $host = $this->serverHost();
if ($httpProtocol !== 'https') { if ($httpProtocol !== 'https') {
$host .= ":{$this->port()}"; $host .= ":{$this->port()}";

View File

@@ -57,15 +57,15 @@ class ControlConnection
$this->proxyManager->createTcpProxy($this->clientId, $data); $this->proxyManager->createTcpProxy($this->clientId, $data);
} }
public function authenticate(string $sharedHost, ?string $subdomain, ?string $hostname) 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', 'type' => 'http',
'host' => $sharedHost, 'host' => $sharedHost,
'server_host' => $serverHost,
'subdomain' => empty($subdomain) ? null : $subdomain, 'subdomain' => empty($subdomain) ? null : $subdomain,
'hostname' => empty($hostname) ? null : $hostname,
], ],
])); ]));
} }

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,9 @@ class Factory
return $this; return $this;
} }
public function share($sharedUrl, $subdomain = null, $hostname = null) public function share($sharedUrl, $subdomain = null, $serverHost = null)
{ {
app('expose.client')->share($sharedUrl, $subdomain, $hostname); app('expose.client')->share($sharedUrl, $subdomain, $serverHost);
return $this; return $this;
} }
@@ -116,6 +130,15 @@ class Factory
return $this; 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;
}
protected function addRoutes() protected function addRoutes()
{ {
$this->router->get('/', DashboardController::class); $this->router->get('/', DashboardController::class);
@@ -127,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

@@ -6,8 +6,8 @@ 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;
@@ -74,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,
@@ -85,17 +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 = $this->rewriteResponseHeaders($response); $response = $this->rewriteResponseHeaders($response);
$response->buffer = str($response); $response->buffer = Message::toString($response);
} }
$this->sendChunkToServer($response->buffer, $proxyConnection); $this->sendChunkToServer($response->buffer, $proxyConnection);
@@ -103,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;

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

@@ -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,46 +3,62 @@
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} {--hostname=} {--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()
{ {
if (! empty($this->option('hostname')) && ! empty($this->option('subdomain'))) { $auth = $this->option('auth') ?? config('expose.auth_token', '');
$this->error('You can only specify one. Either a custom hostname or a subdomain.'); $this->info('Using auth token: '.$auth, OutputInterface::VERBOSITY_DEBUG);
return; if (strstr($this->argument('host'), 'host.docker.internal')) {
config(['expose.dns' => true]);
} }
$this->configureConnectionLogger(); 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( ->share(
$this->argument('host'), $this->argument('host'),
explode(',', $this->option('subdomain')), $subdomains,
$this->option('hostname') $domain
) )
->createHttpServer() ->createHttpServer()
->run(); ->run();

View File

@@ -4,17 +4,17 @@ namespace App\Commands;
class ShareCurrentWorkingDirectoryCommand extends ShareCommand class ShareCurrentWorkingDirectoryCommand extends ShareCommand
{ {
protected $signature = 'share-cwd {host?} {--hostname=} {--subdomain=} {--auth=}'; protected $signature = 'share-cwd {host?} {--subdomain=} {--auth=} {--basicAuth=} {--dns=} {--domain=}';
public function handle() public function handle()
{ {
$subdomain = $this->detectName(); $folderName = $this->detectName();
$host = $this->prepareSharedHost($subdomain.'.'.$this->detectTld()); $host = $this->prepareSharedHost($folderName.'.'.$this->detectTld());
$this->input->setArgument('host', $host); $this->input->setArgument('host', $host);
if (! $this->option('subdomain') && ! $this->option('hostname')) { if (! $this->option('subdomain')) {
$this->input->setOption('subdomain', $subdomain); $this->input->setOption('subdomain', str_replace('.', '-', $folderName));
} }
parent::handle(); parent::handle();
@@ -56,17 +56,22 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
} }
} }
return str_replace('.', '-', basename($projectPath)); return basename($projectPath);
} }
protected function prepareSharedHost($host): string protected function detectProtocol($host): string
{ {
$certificateFile = ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']).DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'Certificates'.DIRECTORY_SEPARATOR.$host.'.crt'; $certificateFile = ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']).DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'Certificates'.DIRECTORY_SEPARATOR.$host.'.crt';
if (file_exists($certificateFile)) { if (file_exists($certificateFile)) {
return 'https://'.$host; return 'https://';
} }
return $host; return config('expose.default_https', false) ? 'https://' : 'http://';
}
protected function prepareSharedHost($host): string
{
return $this->detectProtocol($host).$host;
} }
} }

View File

@@ -3,35 +3,23 @@
namespace App\Commands; namespace App\Commands;
use App\Client\Factory; use App\Client\Factory;
use App\Logger\CliRequestLogger;
use LaravelZero\Framework\Commands\Command;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
class SharePortCommand extends Command class SharePortCommand extends ServerAwareCommand
{ {
protected $signature = 'share-port {port} {--auth=}'; protected $signature = 'share-port {port} {--auth=}';
protected $description = 'Share a local port with a remote expose server'; protected $description = 'Share a local port 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', '');
(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)
->createClient() ->createClient()
->sharePort($this->argument('port')) ->sharePort($this->argument('port'))
->createHttpServer() ->createHttpServer()

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,7 @@ use Ratchet\ConnectionInterface;
interface ConnectionManager interface ConnectionManager
{ {
public function storeConnection(string $host, ?string $subdomain, ?string $hostname, 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 storeTcpConnection(int $port, ConnectionInterface $connection): ControlConnection;
@@ -20,9 +20,7 @@ interface ConnectionManager
public function removeControlConnection($connection); public function removeControlConnection($connection);
public function findControlConnectionForSubdomain($subdomain): ?ControlConnection; public function findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost): ?ControlConnection;
public function findControlConnectionForHostname(string $hostname): ?ControlConnection;
public function findControlConnectionForClientId(string $clientId): ?ControlConnection; public function findControlConnectionForClientId(string $clientId): ?ControlConnection;
@@ -31,4 +29,8 @@ interface ConnectionManager
public function getConnectionsForAuthToken(string $authToken): array; public function getConnectionsForAuthToken(string $authToken): array;
public function getTcpConnectionsForAuthToken(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

@@ -1,22 +0,0 @@
<?php
namespace App\Contracts;
use React\Promise\PromiseInterface;
interface HostnameRepository
{
public function getHostnames(): PromiseInterface;
public function getHostnameById($id): PromiseInterface;
public function getHostnameByName(string $name): PromiseInterface;
public function getHostnamesByUserId($id): PromiseInterface;
public function getHostnamesByUserIdAndName($id, $name): PromiseInterface;
public function deleteHostnameForUserId($userId, $hostnameId): PromiseInterface;
public function storeHostname(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

@@ -12,6 +12,10 @@ interface SubdomainRepository
public function getSubdomainByName(string $name): 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 getSubdomainsByUserId($id): PromiseInterface;
public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface; public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface;

View File

@@ -10,11 +10,15 @@ interface UserRepository
public function getUserById($id): PromiseInterface; public function getUserById($id): PromiseInterface;
public function paginateUsers(int $perPage, int $currentPage): 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', 'Time', '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,18 +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->getStartTime()->toDateTimeString(), 'time' => $loggedRequest->getStartTime()->isToday() ? $loggedRequest->getStartTime()->toTimeString() : $loggedRequest->getStartTime()->toDateTimeString(),
$loggedRequest->getDuration().'ms', '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;
@@ -48,6 +50,7 @@ class LoggedRequest implements \JsonSerializable
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
#[\ReturnTypeWillChange]
public function jsonSerialize() public function jsonSerialize()
{ {
$data = [ $data = [
@@ -171,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) {
@@ -211,4 +214,23 @@ class LoggedRequest implements \JsonSerializable
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

@@ -15,6 +15,7 @@ class AppServiceProvider extends ServiceProvider
{ {
public function boot() public function boot()
{ {
UriFactory::registerScheme('capacitor', Uri::class);
UriFactory::registerScheme('chrome-extension', Uri::class); UriFactory::registerScheme('chrome-extension', Uri::class);
} }
@@ -37,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

@@ -1,35 +0,0 @@
<?php
namespace App\Server\Connections;
class ConnectionConfiguration
{
protected $hostname;
protected $subdomain;
private function __construct($subdomain, $hostname)
{
$this->subdomain = $subdomain;
$this->hostname = $hostname;
}
public static function withSubdomain($subdomain)
{
return new static($subdomain, null);
}
public static function withHostname($hostname)
{
return new static(null, $hostname);
}
public function getSubdomain()
{
return $this->subdomain;
}
public function getHostname()
{
return $this->hostname;
}
}

View File

@@ -3,6 +3,8 @@
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\Http\QueryParameters;
use App\Server\Exceptions\NoFreePortAvailable; use App\Server\Exceptions\NoFreePortAvailable;
@@ -24,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)
@@ -43,32 +53,41 @@ class ConnectionManager implements ConnectionManagerContract
}); });
} }
public function storeConnection(string $host, ?string $subdomain, ?string $hostname, 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;
if (! is_null($hostname) && $hostname !== '') {
$subdomain = '';
} else {
$subdomain = $subdomain ?? $this->subdomainGenerator->generateSubdomain();
}
$storedConnection = new ControlConnection( $storedConnection = new ControlConnection(
$connection, $connection,
$host, $host,
$subdomain, $subdomain ?? $this->subdomainGenerator->generateSubdomain(),
$hostname,
$clientId, $clientId,
$serverHost,
$this->getAuthTokenFromConnection($connection) $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 public function storeTcpConnection(int $port, ConnectionInterface $connection): ControlConnection
{ {
$clientId = (string) uniqid(); $clientId = (string) uniqid();
@@ -85,6 +104,8 @@ class ConnectionManager implements ConnectionManagerContract
$this->connections[] = $storedConnection; $this->connections[] = $storedConnection;
$this->statisticsCollector->portShared($this->getAuthTokenFromConnection($connection));
return $storedConnection; return $storedConnection;
} }
@@ -150,17 +171,10 @@ class ConnectionManager implements ConnectionManagerContract
} }
} }
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;
});
}
public function findControlConnectionForHostname($hostname): ?ControlConnection
{
return collect($this->connections)->last(function ($connection) use ($hostname) {
return $connection->hostname == $hostname;
}); });
} }
@@ -171,6 +185,20 @@ 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;

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,22 +13,24 @@ class ControlConnection
/** @var ConnectionInterface */ /** @var ConnectionInterface */
public $socket; public $socket;
public $host; public $host;
public $serverHost;
public $authToken; public $authToken;
public $subdomain; public $subdomain;
public $hostname;
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 $hostname, string $clientId, string $authToken = '') 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->hostname = $hostname;
$this->client_id = $clientId; $this->client_id = $clientId;
$this->authToken = $authToken; $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)
@@ -63,10 +66,12 @@ class ControlConnection
return [ return [
'type' => 'http', '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, 'auth_token' => $this->authToken,
'subdomain' => $this->subdomain, 'subdomain' => $this->subdomain,
'hostname' => $this->hostname,
'shared_at' => $this->shared_at, 'shared_at' => $this->shared_at,
]; ];
} }

View File

@@ -76,6 +76,7 @@ class TcpControlConnection extends ControlConnection
return [ return [
'type' => 'tcp', 'type' => 'tcp',
'port' => $this->port, 'port' => $this->port,
'auth_token' => $this->authToken,
'client_id' => $this->client_id, 'client_id' => $this->client_id,
'shared_port' => $this->shared_port, 'shared_port' => $this->shared_port,
'shared_at' => $this->shared_at, 'shared_at' => $this->shared_at,

View File

@@ -1,14 +1,14 @@
<?php <?php
namespace App\Server\HostnameRepository; namespace App\Server\DomainRepository;
use App\Contracts\HostnameRepository; use App\Contracts\DomainRepository;
use Clue\React\SQLite\DatabaseInterface; use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result; use Clue\React\SQLite\Result;
use React\Promise\Deferred; use React\Promise\Deferred;
use React\Promise\PromiseInterface; use React\Promise\PromiseInterface;
class DatabaseHostnameRepository implements HostnameRepository class DatabaseDomainRepository implements DomainRepository
{ {
/** @var DatabaseInterface */ /** @var DatabaseInterface */
protected $database; protected $database;
@@ -18,12 +18,12 @@ class DatabaseHostnameRepository implements HostnameRepository
$this->database = $database; $this->database = $database;
} }
public function getHostnames(): PromiseInterface public function getDomains(): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->database $this->database
->query('SELECT * FROM hostnames ORDER by created_at DESC') ->query('SELECT * FROM domains ORDER by created_at DESC')
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows); $deferred->resolve($result->rows);
}); });
@@ -31,12 +31,12 @@ class DatabaseHostnameRepository implements HostnameRepository
return $deferred->promise(); return $deferred->promise();
} }
public function getHostnameById($id): PromiseInterface public function getDomainById($id): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->database $this->database
->query('SELECT * FROM hostnames WHERE id = :id', ['id' => $id]) ->query('SELECT * FROM domains WHERE id = :id', ['id' => $id])
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0] ?? null); $deferred->resolve($result->rows[0] ?? null);
}); });
@@ -44,12 +44,12 @@ class DatabaseHostnameRepository implements HostnameRepository
return $deferred->promise(); return $deferred->promise();
} }
public function getHostnameByName(string $name): PromiseInterface public function getDomainByName(string $name): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->database $this->database
->query('SELECT * FROM hostnames WHERE hostname = :name', ['name' => $name]) ->query('SELECT * FROM domains WHERE domain = :name', ['name' => $name])
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0] ?? null); $deferred->resolve($result->rows[0] ?? null);
}); });
@@ -57,12 +57,12 @@ class DatabaseHostnameRepository implements HostnameRepository
return $deferred->promise(); return $deferred->promise();
} }
public function getHostnamesByUserId($id): PromiseInterface public function getDomainsByUserId($id): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->database $this->database
->query('SELECT * FROM hostnames WHERE user_id = :user_id ORDER by created_at DESC', [ ->query('SELECT * FROM domains WHERE user_id = :user_id ORDER by created_at DESC', [
'user_id' => $id, 'user_id' => $id,
]) ])
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
@@ -72,24 +72,18 @@ class DatabaseHostnameRepository implements HostnameRepository
return $deferred->promise(); return $deferred->promise();
} }
public function storeHostname(array $data): PromiseInterface public function storeDomain(array $data): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->getHostnameByName($data['hostname']) $this->getDomainByName($data['domain'])
->then(function ($registeredHostname) use ($data, $deferred) { ->then(function ($registeredDomain) use ($data, $deferred) {
if (! is_null($registeredHostname)) {
$deferred->resolve(null);
return;
}
$this->database->query(" $this->database->query("
INSERT INTO hostnames (user_id, hostname, created_at) INSERT INTO domains (user_id, domain, created_at)
VALUES (:user_id, :hostname, DATETIME('now')) VALUES (:user_id, :domain, DATETIME('now'))
", $data) ", $data)
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
$this->database->query('SELECT * FROM hostnames WHERE id = :id', ['id' => $result->insertId]) $this->database->query('SELECT * FROM domains 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]);
}); });
@@ -99,12 +93,12 @@ class DatabaseHostnameRepository implements HostnameRepository
return $deferred->promise(); return $deferred->promise();
} }
public function getHostnamesByUserIdAndName($id, $name): PromiseInterface public function getDomainsByUserIdAndName($id, $name): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->database $this->database
->query('SELECT * FROM hostnames WHERE user_id = :user_id AND hostname = :name ORDER by created_at DESC', [ ->query('SELECT * FROM domains WHERE user_id = :user_id AND domain = :name ORDER by created_at DESC', [
'user_id' => $id, 'user_id' => $id,
'name' => $name, 'name' => $name,
]) ])
@@ -115,12 +109,12 @@ class DatabaseHostnameRepository implements HostnameRepository
return $deferred->promise(); return $deferred->promise();
} }
public function deleteHostnameForUserId($userId, $hostnameId): PromiseInterface public function deleteDomainForUserId($userId, $domainId): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->database->query('DELETE FROM hostnames WHERE id = :id AND user_id = :user_id', [ $this->database->query('DELETE FROM domains WHERE id = :id AND user_id = :user_id', [
'id' => $hostnameId, 'id' => $domainId,
'user_id' => $userId, 'user_id' => $userId,
]) ])
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
@@ -129,4 +123,13 @@ class DatabaseHostnameRepository implements HostnameRepository
return $deferred->promise(); return $deferred->promise();
} }
public function updateDomain($id, array $data): PromiseInterface
{
$deferred = new Deferred();
// TODO
return $deferred->promise();
}
} }

View File

@@ -3,20 +3,27 @@
namespace App\Server; namespace App\Server;
use App\Contracts\ConnectionManager as ConnectionManagerContract; use App\Contracts\ConnectionManager as ConnectionManagerContract;
use App\Contracts\HostnameRepository; 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\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\Http\Controllers\Admin\DeleteHostnameController; use App\Server\DomainRepository\DatabaseDomainRepository;
use App\Server\Http\Controllers\Admin\DeleteSubdomainController; 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\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\GetTcpConnectionsController;
use App\Server\Http\Controllers\Admin\GetUserDetailsController; use App\Server\Http\Controllers\Admin\GetUserDetailsController;
use App\Server\Http\Controllers\Admin\GetUsersController; use App\Server\Http\Controllers\Admin\GetUsersController;
@@ -25,13 +32,17 @@ 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\StoreHostnameController; 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\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;
@@ -131,18 +142,28 @@ class Factory
$this->router->get('/sites', ListSitesController::class, $adminCondition); $this->router->get('/sites', ListSitesController::class, $adminCondition);
$this->router->get('/tcp', ListTcpConnectionsController::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->get('/api/users/{id}', GetUserDetailsController::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->post('/api/subdomains', StoreSubdomainController::class, $adminCondition);
$this->router->delete('/api/subdomains/{subdomain}', DeleteSubdomainController::class, $adminCondition); $this->router->delete('/api/subdomains/{subdomain}', DeleteSubdomainController::class, $adminCondition);
$this->router->post('/api/hostnames', StoreHostnameController::class, $adminCondition);
$this->router->delete('/api/hostnames/{hostname}', DeleteHostnameController::class, $adminCondition);
$this->router->delete('/api/users/{id}', DeleteUsersController::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->get('/api/tcp', GetTcpConnectionsController::class, $adminCondition);
$this->router->delete('/api/tcp/{id}', DisconnectTcpConnectionController::class, $adminCondition); $this->router->delete('/api/tcp/{id}', DisconnectTcpConnectionController::class, $adminCondition);
} }
@@ -181,10 +202,12 @@ class Factory
$this->bindConfiguration() $this->bindConfiguration()
->bindSubdomainGenerator() ->bindSubdomainGenerator()
->bindUserRepository() ->bindUserRepository()
->bindLoggerRepository()
->bindSubdomainRepository() ->bindSubdomainRepository()
->bindHostnameRepository() ->bindDomainRepository()
->bindDatabase() ->bindDatabase()
->ensureDatabaseIsInitialized() ->ensureDatabaseIsInitialized()
->registerStatisticsCollector()
->bindConnectionManager() ->bindConnectionManager()
->addAdminRoutes(); ->addAdminRoutes();
@@ -222,16 +245,25 @@ class Factory
protected function bindSubdomainRepository() protected function bindSubdomainRepository()
{ {
app()->singleton(SubdomainRepository::class, function () { app()->singleton(SubdomainRepository::class, function () {
return app(config('expose.admin.subdomain_repository')); return app(config('expose.admin.subdomain_repository', DatabaseSubdomainRepository::class));
}); });
return $this; return $this;
} }
protected function bindHostnameRepository() protected function bindLoggerRepository()
{ {
app()->singleton(HostnameRepository::class, function () { app()->singleton(LoggerRepository::class, function () {
return app(config('expose.admin.hostname_repository')); 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; return $this;
@@ -263,8 +295,8 @@ class Factory
->files() ->files()
->ignoreDotFiles(true) ->ignoreDotFiles(true)
->in(database_path('migrations')) ->in(database_path('migrations'))
->sortByName() ->name('*.sql')
->name('*.sql'); ->sortByName();
/** @var SplFileInfo $migration */ /** @var SplFileInfo $migration */
foreach ($migrations as $migration) { foreach ($migrations as $migration) {
@@ -280,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

@@ -1,44 +0,0 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\HostnameRepository;
use App\Contracts\UserRepository;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class DeleteHostnameController extends AdminController
{
protected $keepConnectionOpen = true;
/** @var HostnameRepository */
protected $hostnameRepository;
/** @var UserRepository */
protected $userRepository;
public function __construct(UserRepository $userRepository, HostnameRepository $hostnameRepository)
{
$this->userRepository = $userRepository;
$this->hostnameRepository = $hostnameRepository;
}
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->hostnameRepository->deleteHostnameForUserId($user['id'], $request->get('hostname'))
->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,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,6 +3,7 @@
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 App\Server\Connections\ControlConnection;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -10,31 +11,55 @@ 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())
->filter(function ($connection) {
return get_class($connection) === ControlConnection::class;
})
->map(function ($site, $siteId) {
$site = $site->toArray();
$site['id'] = $siteId;
return $site; $sites = collect($this->connectionManager->getConnections())
})->values(), ->filter(function ($connection) {
]) 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;
})->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

@@ -3,6 +3,7 @@
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\TcpControlConnection; use App\Server\Connections\TcpControlConnection;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -10,32 +11,55 @@ use Ratchet\ConnectionInterface;
class GetTcpConnectionsController extends AdminController class GetTcpConnectionsController 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([ $connections = collect($this->connectionManager->getConnections())
'tcp_connections' => collect($this->connectionManager->getConnections()) ->filter(function ($connection) {
->filter(function ($connection) { return get_class($connection) === TcpControlConnection::class;
return get_class($connection) === TcpControlConnection::class; })
}) ->map(function ($site, $siteId) use (&$authTokens) {
->map(function ($site, $siteId) { $site = $site->toArray();
$site = $site->toArray(); $site['id'] = $siteId;
$site['id'] = $siteId; $authTokens[] = $site['auth_token'];
return $site; return $site;
}) })
->values(), ->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

@@ -2,7 +2,6 @@
namespace App\Server\Http\Controllers\Admin; namespace App\Server\Http\Controllers\Admin;
use App\Contracts\HostnameRepository;
use App\Contracts\SubdomainRepository; use App\Contracts\SubdomainRepository;
use App\Contracts\UserRepository; use App\Contracts\UserRepository;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -18,36 +17,43 @@ class GetUserDetailsController extends AdminController
/** @var SubdomainRepository */ /** @var SubdomainRepository */
protected $subdomainRepository; protected $subdomainRepository;
/** @var HostnameRepository */ public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository)
protected $hostnameRepository;
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository, HostnameRepository $hostnameRepository)
{ {
$this->userRepository = $userRepository; $this->userRepository = $userRepository;
$this->subdomainRepository = $subdomainRepository; $this->subdomainRepository = $subdomainRepository;
$this->hostnameRepository = $hostnameRepository;
} }
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
{ {
$this->userRepository $id = $request->get('id');
->getUserById($request->get('id'))
->then(function ($user) use ($httpConnection, $request) {
$this->subdomainRepository->getSubdomainsByUserId($request->get('id'))
->then(function ($subdomains) use ($httpConnection, $user, $request) {
$this->hostnameRepository->getHostnamesByUserId($request->get('id'))
->then(function ($hostnames) use ($httpConnection, $user, $subdomains) {
$httpConnection->send(
respond_json([
'user' => $user,
'subdomains' => $subdomains,
'hostnames' => $hostnames,
])
);
$httpConnection->close(); 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,7 +21,7 @@ class GetUsersController extends AdminController
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
{ {
$this->userRepository $this->userRepository
->paginateUsers(20, (int) $request->get('page', 1)) ->paginateUsers($request->get('search', ''), (int) $request->get('perPage', 20), (int) $request->get('page', 1))
->then(function ($paginated) use ($httpConnection) { ->then(function ($paginated) use ($httpConnection) {
$httpConnection->send( $httpConnection->send(
respond_json(['paginated' => $paginated]) respond_json(['paginated' => $paginated])

View File

@@ -4,7 +4,6 @@ namespace App\Server\Http\Controllers\Admin;
use App\Contracts\ConnectionManager; use App\Contracts\ConnectionManager;
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;
@@ -26,17 +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' => collect($this->connectionManager->getConnections())
->filter(function ($connection) {
return get_class($connection) === ControlConnection::class;
})
->map(function ($site, $siteId) {
$site = $site->toArray();
$site['id'] = $siteId;
return $site;
})
->values(),
]); ]);
$httpConnection->send( $httpConnection->send(

View File

@@ -4,7 +4,6 @@ namespace App\Server\Http\Controllers\Admin;
use App\Contracts\ConnectionManager; use App\Contracts\ConnectionManager;
use App\Server\Configuration; use App\Server\Configuration;
use App\Server\Connections\TcpControlConnection;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
@@ -26,17 +25,6 @@ class ListTcpConnectionsController extends AdminController
$sites = $this->getView($httpConnection, 'server.tcp.index', [ $sites = $this->getView($httpConnection, 'server.tcp.index', [
'scheme' => $this->configuration->port() === 443 ? 'https' : 'http', 'scheme' => $this->configuration->port() === 443 ? 'https' : 'http',
'configuration' => $this->configuration, 'configuration' => $this->configuration,
'connections' => collect($this->connectionManager->getConnections())
->filter(function ($connection) {
return get_class($connection) === TcpControlConnection::class;
})
->map(function ($connection, $connectionId) {
$connection = $connection->toArray();
$connection['id'] = $connectionId;
return $connection;
})
->values(),
]); ]);
$httpConnection->send( $httpConnection->send(

View File

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

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

@@ -2,32 +2,32 @@
namespace App\Server\Http\Controllers\Admin; namespace App\Server\Http\Controllers\Admin;
use App\Contracts\HostnameRepository; use App\Contracts\DomainRepository;
use App\Contracts\UserRepository; use App\Contracts\UserRepository;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
class StoreHostnameController extends AdminController class StoreDomainController extends AdminController
{ {
protected $keepConnectionOpen = true; protected $keepConnectionOpen = true;
/** @var HostnameRepository */ /** @var DomainRepository */
protected $hostnameRepository; protected $domainRepository;
/** @var UserRepository */ /** @var UserRepository */
protected $userRepository; protected $userRepository;
public function __construct(UserRepository $userRepository, HostnameRepository $hostnameRepository) public function __construct(UserRepository $userRepository, DomainRepository $domainRepository)
{ {
$this->userRepository = $userRepository; $this->userRepository = $userRepository;
$this->hostnameRepository = $hostnameRepository; $this->domainRepository = $domainRepository;
} }
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
{ {
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'hostname' => 'required', 'domain' => 'required',
], [ ], [
'required' => 'The :attribute field is required.', 'required' => 'The :attribute field is required.',
]); ]);
@@ -39,7 +39,8 @@ class StoreHostnameController extends AdminController
return; return;
} }
$this->userRepository->getUserByToken($request->get('auth_token', '')) $this->userRepository
->getUserByToken($request->get('auth_token', ''))
->then(function ($user) use ($httpConnection, $request) { ->then(function ($user) use ($httpConnection, $request) {
if (is_null($user)) { if (is_null($user)) {
$httpConnection->send(respond_json(['error' => 'The user does not exist'], 404)); $httpConnection->send(respond_json(['error' => 'The user does not exist'], 404));
@@ -48,8 +49,8 @@ class StoreHostnameController extends AdminController
return; return;
} }
if ($user['can_specify_hostnames'] === 0) { if ($user['can_specify_domains'] === 0) {
$httpConnection->send(respond_json(['error' => 'The user is not allowed to reserve hostnames.'], 401)); $httpConnection->send(respond_json(['error' => 'The user is not allowed to reserve custom domains.'], 401));
$httpConnection->close(); $httpConnection->close();
return; return;
@@ -57,19 +58,13 @@ class StoreHostnameController extends AdminController
$insertData = [ $insertData = [
'user_id' => $user['id'], 'user_id' => $user['id'],
'hostname' => $request->get('hostname'), 'domain' => $request->get('domain'),
]; ];
$this->hostnameRepository $this->domainRepository
->storeHostname($insertData) ->storeDomain($insertData)
->then(function ($hostname) use ($httpConnection) { ->then(function ($domain) use ($httpConnection) {
if (is_null($hostname)) { $httpConnection->send(respond_json(['domain' => $domain], 200));
$httpConnection->send(respond_json(['error' => 'The hostname is already taken.'], 422));
$httpConnection->close();
return;
}
$httpConnection->send(respond_json(['hostname' => $hostname], 200));
$httpConnection->close(); $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

@@ -4,6 +4,7 @@ namespace App\Server\Http\Controllers\Admin;
use App\Contracts\SubdomainRepository; use App\Contracts\SubdomainRepository;
use App\Contracts\UserRepository; use App\Contracts\UserRepository;
use App\Server\Configuration;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
@@ -18,10 +19,14 @@ class StoreSubdomainController extends AdminController
/** @var UserRepository */ /** @var UserRepository */
protected $userRepository; protected $userRepository;
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository) /** @var Configuration */
protected $configuration;
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository, Configuration $configuration)
{ {
$this->userRepository = $userRepository; $this->userRepository = $userRepository;
$this->subdomainRepository = $subdomainRepository; $this->subdomainRepository = $subdomainRepository;
$this->configuration = $configuration;
} }
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
@@ -39,7 +44,8 @@ class StoreSubdomainController extends AdminController
return; return;
} }
$this->userRepository->getUserByToken($request->get('auth_token', '')) $this->userRepository
->getUserByToken($request->get('auth_token', ''))
->then(function ($user) use ($httpConnection, $request) { ->then(function ($user) use ($httpConnection, $request) {
if (is_null($user)) { if (is_null($user)) {
$httpConnection->send(respond_json(['error' => 'The user does not exist'], 404)); $httpConnection->send(respond_json(['error' => 'The user does not exist'], 404));
@@ -55,20 +61,22 @@ class StoreSubdomainController extends AdminController
return; 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 = [ $insertData = [
'user_id' => $user['id'], 'user_id' => $user['id'],
'subdomain' => $request->get('subdomain'), 'subdomain' => $request->get('subdomain'),
'domain' => $request->get('domain', $this->configuration->hostname()),
]; ];
$this->subdomainRepository $this->subdomainRepository
->storeSubdomain($insertData) ->storeSubdomain($insertData)
->then(function ($subdomain) use ($httpConnection) { ->then(function ($subdomain) use ($httpConnection) {
if (is_null($subdomain)) {
$httpConnection->send(respond_json(['error' => 'The subdomain is already taken.'], 422));
$httpConnection->close();
return;
}
$httpConnection->send(respond_json(['subdomain' => $subdomain], 200)); $httpConnection->send(respond_json(['subdomain' => $subdomain], 200));
$httpConnection->close(); $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,10 +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_hostnames' => (int) $request->get('can_specify_hostnames'),
'can_specify_subdomains' => (int) $request->get('can_specify_subdomains'), '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'), '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,19 +3,18 @@
namespace App\Server\Http\Controllers; namespace App\Server\Http\Controllers;
use App\Contracts\ConnectionManager; use App\Contracts\ConnectionManager;
use App\Contracts\HostnameRepository; use App\Contracts\DomainRepository;
use App\Contracts\SubdomainRepository; use App\Contracts\SubdomainRepository;
use App\Contracts\UserRepository; use App\Contracts\UserRepository;
use App\Http\QueryParameters; use App\Http\QueryParameters;
use App\Server\Connections\ConnectionConfiguration; use App\Server\Configuration;
use App\Server\Exceptions\NoFreePortAvailable; use App\Server\Exceptions\NoFreePortAvailable;
use Illuminate\Support\Str; 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\PromiseInterface; use React\Promise\PromiseInterface;
use function React\Promise\reject; use function React\Promise\reject;
use function React\Promise\resolve as resolvePromise;
use stdClass; use stdClass;
class ControlMessageController implements MessageComponentInterface class ControlMessageController implements MessageComponentInterface
@@ -29,15 +28,19 @@ class ControlMessageController implements MessageComponentInterface
/** @var SubdomainRepository */ /** @var SubdomainRepository */
protected $subdomainRepository; protected $subdomainRepository;
/** @var HostnameRepository */ /** @var DomainRepository */
protected $hostnameRepository; protected $domainRepository;
public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository, SubdomainRepository $subdomainRepository, HostnameRepository $hostnameRepository) /** @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->subdomainRepository = $subdomainRepository;
$this->hostnameRepository = $hostnameRepository; $this->domainRepository = $domainRepository;
$this->configuration = $configuration;
} }
/** /**
@@ -94,7 +97,42 @@ 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 ($user) use ($connection) {
$maximumConnectionCount = config('expose.admin.maximum_open_connections_per_user', 0);
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']));
}
if ($maximumConnectionCount > 0 && $connectionCount + 1 > $maximumConnectionCount) {
$connection->send(json_encode([
'event' => 'authenticationFailed',
'data' => [
'message' => config('expose.admin.messages.maximum_connection_count'),
],
]));
$connection->close();
reject(null);
}
return $user;
})
->then(function ($user) use ($connection, $data) { ->then(function ($user) use ($connection, $data) {
if ($data->type === 'http') { if ($data->type === 'http') {
$this->handleHttpConnection($connection, $data, $user); $this->handleHttpConnection($connection, $data, $user);
@@ -112,26 +150,56 @@ 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) protected function handleHttpConnection(ConnectionInterface $connection, $data, $user = null)
{ {
$this->hasValidConfiguration($connection, $data, $user) $this->hasValidDomain($connection, $data->server_host, $user)
->then(function (ConnectionConfiguration $configuration) use ($data, $connection) { ->then(function () use ($connection, $data, $user) {
$data->subdomain = $configuration->getSubdomain(); return $this->hasValidSubdomain($connection, $data->subdomain, $user, $data->server_host);
$data->hostname = $configuration->getHostname(); })
->then(function ($subdomain) use ($data, $connection, $user) {
if ($subdomain === false) {
return;
}
$connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $data->hostname, $connection); $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')); $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([ $connection->send(json_encode([
'event' => 'authenticated', 'event' => 'authenticated',
'data' => [ 'data' => [
'message' => config('expose.admin.messages.message_of_the_day'), 'message' => $connectionInfo->message,
'subdomain' => $connectionInfo->subdomain, 'subdomain' => $connectionInfo->subdomain,
'hostname' => $connectionInfo->hostname, 'server_host' => $connectionInfo->serverHost,
'user' => $user,
'client_id' => $connectionInfo->client_id, 'client_id' => $connectionInfo->client_id,
], ],
])); ]));
}); });
} }
@@ -158,7 +226,8 @@ class ControlMessageController implements MessageComponentInterface
$connection->send(json_encode([ $connection->send(json_encode([
'event' => 'authenticated', 'event' => 'authenticated',
'data' => [ 'data' => [
'message' => config('expose.admin.messages.message_of_the_day'), 'message' => config('expose.admin.messages.resolve_connection_message')($connectionInfo, $user),
'user' => $user,
'port' => $connectionInfo->port, 'port' => $connectionInfo->port,
'shared_port' => $connectionInfo->shared_port, 'shared_port' => $connectionInfo->shared_port,
'client_id' => $connectionInfo->client_id, 'client_id' => $connectionInfo->client_id,
@@ -200,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 resolvePromise(null); return \React\Promise\resolve(null);
} }
$deferred = new Deferred(); $deferred = new Deferred();
@@ -213,37 +282,80 @@ class ControlMessageController implements MessageComponentInterface
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, ?array $user): PromiseInterface 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. * 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)) { if (! is_null($user) && $user['can_specify_subdomains'] === 0 && ! is_null($subdomain)) {
$connection->send(json_encode([ $connection->send(json_encode([
'event' => 'info', 'event' => 'error',
'data' => [ 'data' => [
'message' => config('expose.admin.messages.custom_subdomain_unauthorized').PHP_EOL, 'message' => config('expose.admin.messages.custom_subdomain_unauthorized').PHP_EOL,
], ],
])); ]));
return resolvePromise(ConnectionConfiguration::withSubdomain(null)); return \React\Promise\resolve(null);
} }
/** /**
* Check if the given subdomain is reserved for a different user. * Check if the given subdomain is reserved for a different user.
*/ */
if (! is_null($subdomain)) { if (! is_null($subdomain)) {
return $this->subdomainRepository->getSubdomainByName($subdomain) return $this->subdomainRepository->getSubdomainsByNameAndDomain($subdomain, $serverHost)
->then(function ($foundSubdomain) use ($connection, $subdomain, $user) { ->then(function ($foundSubdomains) use ($connection, $subdomain, $user, $serverHost) {
if (! is_null($foundSubdomain) && ! is_null($user) && $foundSubdomain['user_id'] !== $user['id']) { $ownSubdomain = collect($foundSubdomains)->first(function ($subdomain) use ($user) {
$message = config('expose.admin.messages.subdomain_reserved'); return $subdomain['user_id'] === $user['id'];
});
if (count($foundSubdomains) > 0 && ! is_null($user) && is_null($ownSubdomain)) {
$message = config('expose.admin.messages.subdomain_reserved', '');
$message = str_replace(':subdomain', $subdomain, $message); $message = str_replace(':subdomain', $subdomain, $message);
$connection->send(json_encode([ $connection->send(json_encode([
@@ -254,12 +366,12 @@ class ControlMessageController implements MessageComponentInterface
])); ]));
$connection->close(); $connection->close();
return reject(false); return \React\Promise\resolve(false);
} }
$controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain); $controlConnection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost);
if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain')) { 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 = config('expose.admin.messages.subdomain_taken');
$message = str_replace(':subdomain', $subdomain, $message); $message = str_replace(':subdomain', $subdomain, $message);
@@ -271,84 +383,35 @@ class ControlMessageController implements MessageComponentInterface
])); ]));
$connection->close(); $connection->close();
return reject(false); return \React\Promise\resolve(false);
} }
return resolvePromise(ConnectionConfiguration::withSubdomain($subdomain)); return \React\Promise\resolve($subdomain);
}); });
} }
return resolvePromise(ConnectionConfiguration::withSubdomain($subdomain)); return \React\Promise\resolve($subdomain);
}
protected function hasValidHostname(ConnectionInterface $connection, string $hostname, ?array $user): PromiseInterface
{
/**
* Check if the user can specify a custom hostname in the first place.
*/
if (! is_null($user) && $user['can_specify_hostnames'] === 0) {
$connection->send(json_encode([
'event' => 'info',
'data' => [
'message' => config('expose.admin.messages.custom_hostname_unauthorized').PHP_EOL,
],
]));
return reject();
}
/**
* Check if the given hostname is reserved for a different user.
*/
return $this->hostnameRepository->getHostnamesByUserId($user['id'])
->then(function ($foundHostnames) use ($connection, $hostname) {
$foundHostname = collect($foundHostnames)->first(function ($foundHostname) use ($hostname) {
return Str::is($foundHostname['hostname'], $hostname);
});
if (is_null($foundHostname)) {
$message = config('expose.admin.messages.hostname_invalid');
$message = str_replace(':hostname', $hostname, $message);
$connection->send(json_encode([
'event' => 'hostnameTaken',
'data' => [
'message' => $message,
],
]));
$connection->close();
return reject(false);
}
$controlConnection = $this->connectionManager->findControlConnectionForHostname($hostname);
if (! is_null($controlConnection)) {
$message = config('expose.admin.messages.hostname_taken');
$message = str_replace(':hostname', $hostname, $message);
$connection->send(json_encode([
'event' => 'hostnameTaken',
'data' => [
'message' => $message,
],
]));
$connection->close();
return reject(false);
}
return resolvePromise(ConnectionConfiguration::withHostname($hostname));
});
} }
protected function canShareTcpPorts(ConnectionInterface $connection, $data, $user) 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) { if (! is_null($user) && $user['can_share_tcp_ports'] === 0) {
$connection->send(json_encode([ $connection->send(json_encode([
'event' => 'authenticationFailed', 'event' => 'authenticationFailed',
'data' => [ 'data' => [
'message' => config('expose.admin.messages.custom_subdomain_unauthorized'), 'message' => config('expose.admin.messages.tcp_port_sharing_unauthorized'),
], ],
])); ]));
$connection->close(); $connection->close();
@@ -358,13 +421,4 @@ class ControlMessageController implements MessageComponentInterface
return true; return true;
} }
protected function hasValidConfiguration(ConnectionInterface $connection, $data, $user)
{
if (isset($data->hostname) && ! is_null($data->hostname)) {
return $this->hasValidHostname($connection, $data->hostname, $user);
}
return $this->hasValidSubdomain($connection, $data->subdomain, $user);
}
} }

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,18 +28,22 @@ 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);
$hostname = $request->getHost(); $serverHost = $this->detectServerHost($request);
if (is_null($subdomain) && $hostname === $this->configuration->hostname()) { if (is_null($subdomain)) {
$httpConnection->send( $httpConnection->send(
respond_html($this->getView($httpConnection, 'server.homepage'), 200) respond_html($this->getView($httpConnection, 'server.homepage'), 200)
); );
@@ -47,11 +52,7 @@ class TunnelMessageController extends Controller
return; return;
} }
if (! is_null($subdomain)) { $controlConnection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost);
$controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain);
} else {
$controlConnection = $this->connectionManager->findControlConnectionForHostname($hostname);
}
if (is_null($controlConnection)) { if (is_null($controlConnection)) {
$httpConnection->send( $httpConnection->send(
@@ -62,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)
@@ -112,25 +122,19 @@ 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()}";
} }
if (empty($controlConnection->subdomain)) {
$originalHost = $controlConnection->hostname;
} else {
$originalHost = "{$controlConnection->subdomain}.{$host}";
}
$request->headers->set('Host', $controlConnection->host); $request->headers->set('Host', $controlConnection->host);
$request->headers->set('X-Forwarded-Proto', $request->isSecure() ? 'https' : 'http'); $request->headers->set('X-Forwarded-Proto', $request->isSecure() ? 'https' : 'http');
$request->headers->set('X-Expose-Request-ID', uniqid()); $request->headers->set('X-Expose-Request-ID', uniqid());
$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', $originalHost); $request->headers->set('X-Original-Host', "{$controlConnection->subdomain}.{$host}");
$request->headers->set('X-Forwarded-Host', $originalHost); $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

@@ -57,6 +57,38 @@ class DatabaseSubdomainRepository implements SubdomainRepository
return $deferred->promise(); 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 public function getSubdomainsByUserId($id): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
@@ -76,23 +108,14 @@ class DatabaseSubdomainRepository implements SubdomainRepository
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->getSubdomainByName($data['subdomain']) $this->database->query("
->then(function ($registeredSubdomain) use ($data, $deferred) { INSERT INTO subdomains (user_id, subdomain, domain, created_at)
if (! is_null($registeredSubdomain)) { VALUES (:user_id, :subdomain, :domain, DATETIME('now'))
$deferred->resolve(null); ", $data)
->then(function (Result $result) use ($deferred) {
return; $this->database->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $result->insertId])
}
$this->database->query("
INSERT INTO subdomains (user_id, subdomain, created_at)
VALUES (:user_id, :subdomain, DATETIME('now'))
", $data)
->then(function (Result $result) use ($deferred) { ->then(function (Result $result) use ($deferred) {
$this->database->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $result->insertId]) $deferred->resolve($result->rows[0]);
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows[0]);
});
}); });
}); });
@@ -119,7 +142,7 @@ class DatabaseSubdomainRepository implements SubdomainRepository
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->database->query('DELETE FROM subdomains WHERE id = :id AND user_id = :user_id', [ $this->database->query('DELETE FROM subdomains WHERE (id = :id OR subdomain = :id) AND user_id = :user_id', [
'id' => $subdomainId, 'id' => $subdomainId,
'user_id' => $userId, 'user_id' => $userId,
]) ])

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

@@ -36,34 +36,52 @@ class DatabaseUserRepository implements UserRepository
return $deferred->promise(); return $deferred->promise();
} }
public function paginateUsers(int $perPage, int $currentPage): PromiseInterface public function paginateUsers(string $searchQuery, int $perPage, int $currentPage): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
$this->database $this->database
->query('SELECT * FROM users ORDER by created_at DESC LIMIT :limit OFFSET :offset', [ ->query('SELECT COUNT(*) AS count FROM users')
'limit' => $perPage + 1, ->then(function (Result $result) use ($searchQuery, $deferred, $perPage, $currentPage) {
'offset' => $currentPage < 2 ? 0 : ($currentPage - 1) * $perPage, $totalUsers = $result->rows[0]['count'];
])
->then(function (Result $result) use ($deferred, $perPage, $currentPage) {
if (count($result->rows) == $perPage + 1) {
array_pop($result->rows);
$nextPage = $currentPage + 1;
}
$users = collect($result->rows)->map(function ($user) { $query = 'SELECT * FROM users ';
return $this->getUserDetails($user);
})->toArray();
$paginated = [ $bindings = [
'users' => $users, 'limit' => $perPage + 1,
'current_page' => $currentPage, 'offset' => $currentPage < 2 ? 0 : ($currentPage - 1) * $perPage,
'per_page' => $perPage,
'next_page' => $nextPage ?? null,
'previous_page' => $currentPage > 1 ? $currentPage - 1 : null,
]; ];
$deferred->resolve($paginated); 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(); return $deferred->promise();
@@ -96,6 +114,19 @@ class DatabaseUserRepository implements UserRepository
return $deferred->promise(); 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();
}
public function getUserByToken(string $authToken): PromiseInterface public function getUserByToken(string $authToken): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();
@@ -103,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();
@@ -113,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, can_specify_subdomains, can_specify_hostnames, can_share_tcp_ports, created_at) ->then(function ($existingUser) use ($data, $deferred) {
VALUES (:name, :auth_token, :can_specify_subdomains, :can_specify_hostnames, :can_share_tcp_ports, 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();
@@ -131,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,50 +1,56 @@
{ {
"name": "beyondcode/expose", "name": "bitinflow/expose",
"type": "project", "type": "project",
"description": "Expose", "description": "Create public URLs for local sites through any firewall and VPN.",
"keywords": [ "keywords": [
"expose", "expose",
"tunnel", "tunnel",
"ngrok" "ngrok"
], ],
"homepage": "https://sharedwithexpose.com", "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"
} }
], ],
"require": { "require": {
"php": "^7.3.0", "php": "^8.0",
"ext-json": "*" "ext-json": "*",
"laravel-zero/phar-updater": "^1.2"
}, },
"require-dev": { "require-dev": {
"cboden/ratchet": "^0.4.2", "cboden/ratchet": "^0.4.3",
"clue/block-react": "^1.3", "clue/block-react": "^1.4",
"clue/buzz-react": "^2.7", "clue/buzz-react": "^2.9",
"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",
"nikic/php-parser": "^4.4", "octoper/cuzzle": "^3.1",
"nyholm/psr7": "^1.2", "nikic/php-parser": "^v4.10",
"phpunit/phpunit": "^8.5", "nyholm/psr7": "^1.3",
"ratchet/pawl": "^0.3.4", "phpunit/phpunit": "^9.4.3",
"react/http": "^0.8.6", "ratchet/pawl": "^0.3.5",
"react/http": "^1.1.0",
"react/socket": "^1.6", "react/socket": "^1.6",
"react/stream": "^1.1.1", "react/stream": "^1.1.1",
"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": { "config": {
"optimize-autoloader": true, "optimize-autoloader": true,

7893
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

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.
| sharedwithexpose.com 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
@@ -151,6 +205,19 @@ 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 | TCP Port Range
@@ -182,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
@@ -194,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
@@ -206,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
@@ -234,7 +346,7 @@ return [
'subdomain_repository' => \App\Server\SubdomainRepository\DatabaseSubdomainRepository::class, 'subdomain_repository' => \App\Server\SubdomainRepository\DatabaseSubdomainRepository::class,
'hostname_repository' => \App\Server\HostnameRepository\DatabaseHostnameRepository::class, 'logger_repository' => \App\Server\LoggerRepository\NullLogger::class,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -247,15 +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_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.', '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'),
],
],
];

View File

@@ -1 +0,0 @@
ALTER TABLE users ADD can_specify_hostnames BOOLEAN DEFAULT 1;

View File

@@ -0,0 +1 @@
ALTER TABLE users ADD max_connections INTEGER NOT NULL DEFAULT 0;

View File

@@ -0,0 +1 @@
ALTER TABLE users ADD last_shared_at DATETIME;

View File

@@ -1,7 +0,0 @@
CREATE TABLE IF NOT EXISTS hostnames (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
hostname STRING NOT NULL,
created_at DATETIME,
updated_at DATETIME
)

View File

@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS statistics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATE,
shared_sites INTEGER,
shared_ports INTEGER,
unique_shared_sites INTEGER,
unique_shared_ports INTEGER,
incoming_requests INTEGER
)

View File

@@ -0,0 +1,10 @@
ALTER TABLE users ADD can_specify_domains BOOLEAN DEFAULT 1;
ALTER TABLE subdomains ADD domain STRING;
CREATE TABLE IF NOT EXISTS domains (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
domain STRING NOT NULL,
created_at DATETIME,
updated_at DATETIME
)

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
subdomain STRING NOT NULL,
created_at DATETIME
)

View File

@@ -3,7 +3,7 @@ services:
expose: expose:
image: beyondcodegmbh/expose-server:latest image: beyondcodegmbh/expose-server:latest
ports: ports:
- 127.0.0.1:8080:${PORT} - 8080:${PORT}
environment: environment:
port: ${PORT} port: ${PORT}
domain: ${DOMAIN} domain: ${DOMAIN}

9
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
sed -i "s|username|${username}|g" ${exposeConfigPath} && sed -i "s|password|${password}|g" ${exposeConfigPath}
if [[ $# -eq 0 ]]; then
exec /src/expose serve ${domain} --port ${port} --validateAuthTokens
else
exec /src/expose "$@"
fi

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