Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f709886ed | ||
|
|
81d9ddfd03 | ||
| c74cd91ab3 | |||
|
|
ca2f3df587 | ||
| d02152e2b8 | |||
|
|
9d38dd8ede | ||
|
|
ae59561267 | ||
|
|
f9129eefc7 | ||
|
|
e3c3205016 | ||
|
|
0e07cc3b01 | ||
|
|
bf8fef650b | ||
|
|
281ca362bd | ||
|
|
7c620cbe55 | ||
|
|
2ae6c8c2b8 | ||
|
|
b4b26fcc67 | ||
|
|
a959fe100f | ||
|
|
b882fd6bf3 | ||
|
|
e9cc392d16 | ||
|
|
e65540137e | ||
|
|
a9f3f8f43c | ||
|
|
8ffde3c0f8 | ||
|
|
42bf107686 | ||
| de2c993957 | |||
| 08f9258f5c | |||
| cb8a7c00ec | |||
| 5fd94f5487 | |||
| 33ad7a37ae | |||
| 127a63d4a4 | |||
| 31dfa8fc34 | |||
| 0138a6e12d | |||
| bd705ab55d | |||
| 1456e013ec | |||
| 34a344a952 | |||
| 99c00eb649 | |||
| e3a380be10 | |||
| 65b13d8949 | |||
|
|
610904d4b9 | ||
| 9dcb1906f9 | |||
| 5b74ae2415 | |||
| 5e95451c0e | |||
| 23288a99b4 | |||
| 4af70552f5 | |||
| a5c0980f3a | |||
| 153adcfdd1 | |||
|
|
269bd5ea1f | ||
|
|
db7b6282ce | ||
|
|
5ef05e811e | ||
|
|
ef89333873 | ||
|
|
a7f799912b | ||
|
|
425641da57 | ||
|
|
eb8b5b6f74 | ||
|
|
b8545fb767 | ||
|
|
860d816031 | ||
|
|
2ffb79088f | ||
|
|
4ea46e4e24 | ||
|
|
73f0a980d9 | ||
|
|
16f9b6e4a6 | ||
|
|
55f641afc4 | ||
|
|
99b9d67af0 | ||
|
|
f9dd82d909 | ||
|
|
44508f99dc | ||
|
|
f5aaed23ab | ||
|
|
d36f10189d | ||
|
|
cfd6ccdbbc | ||
|
|
0833e91520 | ||
|
|
fce23c7cd1 | ||
|
|
1b9c4e87af | ||
|
|
a6ae512417 | ||
|
|
48922db7c4 | ||
|
|
107ea4f396 | ||
|
|
d163822726 | ||
|
|
fa3e7ed385 | ||
|
|
58e54f5f01 | ||
|
|
99f93eb00e | ||
|
|
8122e8d4a0 | ||
|
|
7a30f92ed1 | ||
|
|
278f561744 | ||
|
|
1dde4d4fc1 | ||
|
|
86f04da9a4 | ||
|
|
75ab2e6a94 | ||
|
|
e83a255b85 | ||
|
|
74a3c9a45d | ||
|
|
fc73f8c4df | ||
|
|
c8e6df0083 | ||
|
|
d720fd3758 | ||
|
|
bc6c6d735b | ||
|
|
495d05679d | ||
|
|
c7a949f04f | ||
|
|
4076b295a5 | ||
|
|
bd947c1c5a | ||
|
|
eee94058a1 | ||
|
|
8e1c0f38b7 | ||
|
|
069e01fe6a | ||
|
|
3e7cd1b41f | ||
|
|
e2d7a5a8d3 | ||
|
|
cd7b28cd30 | ||
|
|
f55fe3f58e | ||
|
|
ba8870e5a3 | ||
|
|
6c0e749edb | ||
|
|
77136affc2 | ||
|
|
d0c4b91965 | ||
|
|
3940d7139f | ||
|
|
0c4465e827 | ||
|
|
df35168d78 | ||
|
|
f8bc4e33e8 | ||
|
|
5497ab7a96 | ||
|
|
85c2bbd372 | ||
|
|
19f1d68c4f | ||
|
|
31e121d1de | ||
|
|
53dff3d0c5 | ||
|
|
5a320e18d4 | ||
|
|
e4963556c0 | ||
|
|
46ac9f3aa0 | ||
|
|
e5392cefcb | ||
|
|
cd6f3a0160 | ||
|
|
972173f9e3 | ||
|
|
78f3c86959 | ||
|
|
43bcdbdad2 | ||
|
|
a4fc0397e0 | ||
|
|
84a972ad55 | ||
|
|
5072f073f6 | ||
|
|
f0ce076571 | ||
|
|
58bcc89fc4 | ||
|
|
d7dc82cd9b | ||
|
|
1aa16526d6 | ||
|
|
1f4e2d99e6 | ||
|
|
413317f930 | ||
|
|
8466b31038 | ||
|
|
309f10a800 | ||
|
|
054698ee64 | ||
|
|
04db1ca0f0 | ||
|
|
73ec99f00b | ||
|
|
65fd3d2a71 | ||
|
|
09bdd1a3fb | ||
|
|
6b068b06ad | ||
|
|
43ee8347e7 | ||
|
|
f1e77a5973 | ||
|
|
f4c566ca40 | ||
|
|
80b32af16a | ||
|
|
46cb6c4a3a | ||
|
|
dda7520818 | ||
|
|
b269fe4476 | ||
|
|
34da0f2242 | ||
|
|
328fb17482 | ||
|
|
e529f5b691 | ||
|
|
316f39401f | ||
|
|
8a3cdb48f5 | ||
|
|
2810791c00 |
5
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# These are supported funding model platforms
|
||||
ko_fi: tildeclub # Replace with a single Liberapay username
|
||||
github: tildeclub
|
||||
custom: https://www.paypal.com/donate?hosted_button_id=DWHSADKJ26HZ8
|
||||
custom: https://donate.tilde.club
|
||||
3
.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
*.swp
|
||||
webmail
|
||||
includes/report
|
||||
report
|
||||
tilde.json
|
||||
|
||||
30
.htaccess
@@ -1,7 +1,23 @@
|
||||
RewriteEngine On
|
||||
RewriteRule ^$ main [QSA]
|
||||
RewriteRule ^index\.php$ wiki.php?page=main [QSA]
|
||||
RewriteRule ^users$ includes/users.php [QSA,L]
|
||||
RewriteRule ^server$ includes/server.php [QSA,L]
|
||||
RewriteCond %{REQUEST_URI} !(/includes/|/media/|tilde.json|humans.txt|/webmail/|/favicon.ico|/~|githook)
|
||||
RewriteRule ^([^\d]+)/?$ wiki.php?page=$1 [QSA]
|
||||
RewriteEngine On
|
||||
|
||||
# Classic query-style links like /?page=main should keep working.
|
||||
RewriteCond %{QUERY_STRING} (^|&)page= [NC]
|
||||
RewriteRule ^$ wiki.php [QSA,L]
|
||||
|
||||
# Default experience: terminal UI.
|
||||
RewriteRule ^$ terminal/ [QSA,L]
|
||||
|
||||
# If someone explicitly requests index.php with a page query, keep classic behavior.
|
||||
RewriteCond %{QUERY_STRING} (^|&)page= [NC]
|
||||
RewriteRule ^index\.php$ wiki.php [QSA,L]
|
||||
|
||||
# Otherwise, index.php also goes to the terminal UI.
|
||||
RewriteRule ^index\.php$ terminal/ [QSA,L]
|
||||
|
||||
# Let real files and directories through untouched.
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# Pretty URLs for wiki pages (no leading digit).
|
||||
RewriteRule ^([^0-9][A-Za-z0-9_-]*)/?$ wiki.php?page=$1 [QSA,L]
|
||||
|
||||
@@ -2,8 +2,8 @@ If you have any questions or problems relating to our service, or this website,
|
||||
|
||||
<form action='/includes/contact.php'>
|
||||
<table>
|
||||
<tr><td>Contact Name:</td> <td> <input type='text' name='contact_name'><br></td></tr>
|
||||
<tr><td>Email Address:</td> <td> <input type='text' name='email_address'><br></td></tr>
|
||||
<tr><td>Contact Name:</td> <td> <input type='text' name='contact_name'></td></tr>
|
||||
<tr><td>Email Address:</td> <td> <input type='text' name='email_address'></td></tr>
|
||||
<tr><td>Subject:</td> <td>
|
||||
<select name="type">
|
||||
<option value="abuse">Abuse</option>
|
||||
@@ -12,7 +12,7 @@ If you have any questions or problems relating to our service, or this website,
|
||||
|
||||
</select><br> </td></tr>
|
||||
<tr><td>Type the word tildeverse here:</td> <td> <input type='textarea' name='tv'></td></tr>
|
||||
<tr><td>Message:</td><td><textarea name='message' rows = "10" cols = "80"></textarea><br><td>
|
||||
<tr><td>Message:</td><td><textarea name='message' rows = "10" cols = "80"></textarea><td>
|
||||
</table>
|
||||
<input type='submit'>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
## creative commons
|
||||
|
||||
# Contributors
|
||||
|
||||
A list of all contributors can be found here: [Commit log for Git Repo](https://github.com/ThunixdotNet/www/commits/branch/master)
|
||||
# Attribution-ShareAlike 4.0 International
|
||||
|
||||
Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
# Donations
|
||||
|
||||
As this server and our projects are all a labor of love and goodwill for the community, operating the thunix server costs money. We love what we do and we love sharing what we do for free, but over time, operating expenses can have a big impact.
|
||||
While this server and our projects are a labor of love and goodwill for the community, operating the thunix server still costs money. We love what we do and we love sharing what we do for free, but over time, operating expenses can have a big impact. In order to keep going, we rely on the good nature of generous people who are willing to donate to us. The price breakdown right now is €80/month.
|
||||
|
||||
That being said, we also rely on the good nature of generous people, who are willing to donate to us. The price breakdown right now is €38/month. So to help with server costs and time spent, you can donate the following ways:
|
||||
If you'd like to assist with server costs and help ensure we can spend time on the projects, you can donate in the following ways:
|
||||
|
||||
<div style="text-align:center;">
|
||||
|
||||
<p>Via BitCoin: 1DFM3qY7XmCxGTFPATSQJVCNjvmWiVubGN</p>
|
||||
<p>You can donate via fosspay here: <a href="https://donate.tilde.club/?project=2"><img src="https://www.gravatar.com/avatar/08ba2126a0dd0cb2efa30b854c7b4252?s=129"></a></p>
|
||||
|
||||
<p>You can donate via Liberapay here: <a href="https://liberapay.com/ub3g33k/donate"><img src="https://liberapay.com/assets/widgets/donate.svg"></a></p>
|
||||
|
||||
<p>Or via Paypal:</a>
|
||||
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_donations" />
|
||||
<input type="hidden" name="business" value="GW2H85HY9VJ3L" />
|
||||
<input type="hidden" name="currency_code" value="USD" />
|
||||
<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif" border="0" name="submit" title="PayPal - The safer, easier way to pay online!" alt="Donate with PayPal button" />
|
||||
<img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1" />
|
||||
</form>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
_Be sure to select the thunix.net project in the dropdown and please add a comment to tell us your thoughts! Thank you!_
|
||||
|
||||
@@ -2,38 +2,65 @@
|
||||
|
||||
**How do I sign up for an account?**
|
||||
|
||||
Simply by going to our [signup page](/signup) and filling in the form. You can ask for help in \#thunix on tilde.chat, or you can [contact us](contact), if you run into any difficulties.
|
||||
- Go to the [signup page](/signup) and fill out the form.
|
||||
- If you get stuck, ask in **#thunix** on **newnet.net**, or [contact us](/contact).
|
||||
|
||||
**How can I request an account recovery or public key replacement?**
|
||||
|
||||
- Email us **from the address you used to register** and tell us:
|
||||
- your username
|
||||
- what you need (recovery / key replacement)
|
||||
- your **new public key** (paste it in the email)
|
||||
- We’ll swap the key and let you know when it’s done.
|
||||
|
||||
**Who is running thunix?**
|
||||
|
||||
The current system administrators are [amcclure](/~amcclure), [ubergeek](/~ubergeek), [Naglfar](/~naglfar), and [fosslinux](/~fosslinux).
|
||||
- Current system administrators: [deepend](/~deepend), [Naglfar](/~naglfar)
|
||||
|
||||
**What happened to the old thunix? Why the name change?**
|
||||
|
||||
The original machine and founder dissappeared without any warning to anyone, including server staff. For this reason, most things were not backed up, and we needed to obtain a new domain name, and a new set of machines.
|
||||
- The original machine and founder disappeared without warning (including to staff).
|
||||
- Most things weren’t backed up, so we rebuilt on new machines and moved to a new domain.
|
||||
|
||||
**I want a new package installed, or I want something changed on Thunix!**
|
||||
|
||||
Excellent! We're looking to make this system useful for the community! You can submit a PR or an issue [here](https://tildegit.org/thunix/ansible) to request the system change.
|
||||
- Good. That’s how systems become useful instead of decorative.
|
||||
- Ask in **#thunix** on **newnet.net**, or [contact us](/contact) with:
|
||||
- what you want
|
||||
- why you want it
|
||||
- whether it needs to be available to everyone or just you
|
||||
|
||||
**Can I get password-based login? Old thunix had it!**
|
||||
|
||||
No. Sorry. Not for shell access. For other integrated services, password auth will be enabled, but not for your ssh connection. We use key based authentication, as it's more secure, and more convienent for you, to be honest.
|
||||
- No. Not for **shell access**.
|
||||
- SSH is **key-based** because it’s more secure and, honestly, less annoying once you’re set up.
|
||||
- Other services (like email) use passwords because that’s how the world works.
|
||||
|
||||
**I want to run {fill in the blank} server, but I can't seem to access it?**
|
||||
**That’s too hard! Can you just open the port up for this service I have running?**
|
||||
|
||||
The only exposed ports to the internet are services as defined in our [ansible playbook.](https://tildegit.org/thunix/ansible) If there is a public service you want to see, open an issue, or do a pull request for it, and we'll probably enable it without much question.
|
||||
|
||||
**That's too hard! Can you just open the port up for this service I have running?**
|
||||
|
||||
No. Due to security issues, we cannot. HOWEVER! You can certainly use an [SSH tunnel](https://duckduckgo.com/?q=ssh+tunnnel) to access it.
|
||||
- No.
|
||||
- If you need access to something you’re running, use an **SSH tunnel**.
|
||||
- Example (adjust ports as needed):
|
||||
- `ssh -L 8080:127.0.0.1:8080 youruser@YOUR_SSH_HOSTNAME`
|
||||
|
||||
**Old thunix did {fill in the blank}, and now it doesn't. Make it work like it used to!**
|
||||
|
||||
There was a huge changeover. Maybe we can get something going old thunix had, and maybe not. You can mention it in the IRC channel, and we'll see what we can do.
|
||||
- There was a big changeover. Some old stuff can come back, some can’t.
|
||||
- Mention it in **#thunix** and we’ll see what’s realistic.
|
||||
|
||||
**How can I access my thunix email?**
|
||||
|
||||
You can use the following for your mail settings (This is Thunderbird's setting screen, but the settings are the same):
|
||||
- Use these settings in Thunderbird, Apple Mail, Outlook, mutt, a ham radio, whatever.
|
||||
|
||||
[](/media/mail.png)
|
||||
## Incoming Mail (IMAP)
|
||||
- **Server:** `thunix.net`
|
||||
- **Username:** `yourusername`
|
||||
- **Password:** your **mail/service** password (not your SSH key)
|
||||
- **Security:** SSL/TLS
|
||||
- **Port:** `993`
|
||||
|
||||
## Outgoing Mail (SMTP)
|
||||
- **Server:** `thunix.net`
|
||||
- **Authentication:** Yes (same username/password as above)
|
||||
- **Security:** STARTTLS
|
||||
- **Port:** `587`
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# GDPR Statement and Privacy Policy
|
||||
|
||||
Thunix takes privacy seriously, and as such remains committed to being in compliance with the GDPR, which took affect on May 25th, 2018.
|
||||
|
||||
## What data do we collect from you?
|
||||
|
||||
Thunix collects your email address, during signup, and IPs you log into the system from, in accordance with technical requirements.
|
||||
|
||||
We also store whatever data you create or maintain in your home directory on Thunix, which may include emails to and from your Thunix account.
|
||||
|
||||
## How long do we retain data about you?
|
||||
|
||||
Your email address is retained only long enough to process you account creation request. IPs you log into from are retained for a maximum of 30 days.
|
||||
|
||||
## How can I request a copy of my data?
|
||||
|
||||
You can submit a request at [contact page](/contact). We will endeavor to provide you a copy of all data we store within 14 days.
|
||||
|
||||
## How can I request removal of my information?
|
||||
|
||||
You can submit a request at [contact page](/contact), and we will endeavor to remove all personal data of yours from our system within 14 days. However, this will include removal of your user account from our system as well, which will prevent access to most of the services provided by Thunix.
|
||||
|
||||
If you request deletion of your account, all of your data will be purged within 14 days from our system.
|
||||
|
||||
## How do you protect my data?
|
||||
|
||||
All data on thunix is encrypted at rest. Logs are maintained only for as long as is technically required (30 days, for troubleshooting of issues). Personally created data, is protected by the UNIX permissions model, as dictated by yourself, however, by default, your personally created data is viewable by all other users of the system, per design.
|
||||
@@ -5,14 +5,10 @@ We're so glad you could drop by and we hope you enjoy your visit. Stay a while!
|
||||
|
||||
## About thunix
|
||||
|
||||
thunix offers Secure Shell (SSH) accounts, Web Hosting, Email Accounts, and many other services. But, most of all, we are a community of users. It was founded by hexhaxtron in the Summer of 2017, and was continued by [amcclure](/~amcclure) and [ubergeek](/~ubergeek) since 2018. We aim to provide the best service possible with a wide variety of features, and we hope you have fun with it!
|
||||
The Thunix project provide Secure Shell (SSH) accounts, Web Hosting, Email Accounts, and many other UNIX-like services. But, most of all, we are a community of users. It was founded by hexhaxtron in the Summer of 2017, and was continued by [ubergeek](/~ubergeek) since 2018 however in 2023 [deepend](/~deepend) has since taken the role. We aim to provide the best service possible with a wide variety of features, and we hope you have fun with it!
|
||||
|
||||
|
||||
## Project Phoenix
|
||||
|
||||
The thunix Phoenix project aims to provide a new user experience for shell users, with integration into the wider Tildeverse network.
|
||||
|
||||
Join us on IRC on irc.tilde.chat/6697 in the #thunix channel, or just click here for a web client: [](https://web.tilde.chat/?join=thunix)
|
||||
Join us on IRC on irc.newnet.net/6697 in the #thunix channel, or just click here for a web client: [](https://newnet.net/chat.php?channel=%23thunix)
|
||||
|
||||
If you want to sign up for an account, simply open our [signup form](/signup) and provide us with:
|
||||
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
# State of the Thunix - July 09, 2019
|
||||
# Season’s Greetings - December 2025
|
||||
|
||||
Another Month!
|
||||
As the year winds down, a sincere thank-you to everyone who’s made Thunix feel like a real community and not just “a server with accounts.” Whether you’re here for SSH, web hosting, email, or the other UNIX-y odds and ends, we’re glad you’re part of this little corner of the internet.
|
||||
|
||||
We are at month 7 for Thunix.net!
|
||||
If you’re taking some quiet time over the holidays, consider doing something wonderfully low-pressure: update your `~/public_html`, publish something on Gopher, poke at the Gemini capsule (`gemini://thunix.net`), or just hang out with us on IRC (`irc.newnet.net:6697` in `#thunix`). No algorithms, no shouting, just people making things.
|
||||
|
||||
Newly, over the past month, we have a wiki now! It's an effort led by
|
||||
contribute via tildegit, by opening a PR at:
|
||||
However you spend the season: take care of yourself, be decent to each other, and we’ll keep doing our best to keep Thunix stable, secure, and fun.
|
||||
|
||||
https://tildegit.org/thunix/wiki
|
||||
# Changes to Terms of Service and Service Updates - April 2025
|
||||
|
||||
It's all in markdown, so it's pretty easy to contribute too. And, if you
|
||||
mess something up? No worries! It's a wiki! We can revert :)
|
||||
We've updated our Terms of Service to clarify rules around running servers—now explicitly prohibited without prior approval. Please take a moment to review these important changes.
|
||||
|
||||
Also, new this month, we've made an official process and tool to provision
|
||||
MySQL/MariaDB databases (It's MariaDB, btw). All you need to do is
|
||||
request one, and we'll gladly add it :)
|
||||
Additionally, due to low usage and maintenance overhead, we've decided to discontinue our BZFlag, Minetest, and Minecraft servers effective immediately. We appreciate everyone who participated and hope you enjoyed your time on these services.
|
||||
|
||||
No donations this month, and costs to operate the server still sit at
|
||||
feel the need to do so, via https://thunix.net/donate.php
|
||||
Thank you for your understanding and continued support!
|
||||
|
||||
Iris is getting some usage too, so make sure you poke in there, via the
|
||||
cli.
|
||||
# Gemini is Live on Thunix! - Nov 2024
|
||||
|
||||
All in all, new features, new stuff, and looking forward to seeing other
|
||||
contributions and new features come in from our users.
|
||||
Hey everyone, exciting news—Thunix now supports Gemini! 🎉
|
||||
|
||||
Your Friendly Neighborhood Sysadmin;
|
||||
ubergeek/ub3geek
|
||||
You can check out our Gemini capsule at gemini://thunix.net. It's simple, fast, and perfect for sharing cool stuff without all the web bloat.
|
||||
|
||||
Got ideas for Gemini content? Let us know! And if you’re new to Gemini, dive in—it’s like the web, but chill. 😎
|
||||
|
||||
Catch you in the capsule! 🚀
|
||||
|
||||
# State of the Thunix - July 2023
|
||||
|
||||
We are on the mend. deepend from tilde.club has taken on running Thunix and has started to build it up on his
|
||||
own infrastructure. Sign ups that come in will be kept in queue until the system is ready
|
||||
for more users.
|
||||
|
||||
More to come very soon.
|
||||
|
||||
25
articles/privacy.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Privacy Policy
|
||||
|
||||
Thunix takes user privacy seriously.
|
||||
|
||||
## What data do we collect?
|
||||
|
||||
We collect your email address during signup and log the IP addresses used to access the system.
|
||||
We also store any data you create or maintain in your home directory, which may include emails to and from your Thunix account.
|
||||
|
||||
## How long do we retain data?
|
||||
|
||||
- Email addresses: Retained for account management.
|
||||
- Login IP addresses: Retained for a maximum of 30 days.
|
||||
|
||||
## Requesting data deletion
|
||||
|
||||
You may submit a deletion request via our [contact page](/contact). Account deletion will also remove all your data, typically within 14 days.
|
||||
|
||||
## How do we protect your data?
|
||||
|
||||
Data is **not encrypted at rest**. Access is controlled by standard UNIX file permissions. Users should **not store sensitive or confidential information** on Thunix.
|
||||
|
||||
Logs are retained only for as long as necessary for troubleshooting (30 days).
|
||||
|
||||
---
|
||||
@@ -1,31 +1,33 @@
|
||||
# Service Status and Information
|
||||
|
||||
## Minetest
|
||||
|
||||
Connect to thunix.net:30000 in your minetest client
|
||||
|
||||
## Minecraft
|
||||
|
||||
Connect to thunix.net in your minecraft client. Running the Paperclip minecraft server.
|
||||
|
||||
## Mail Services
|
||||
|
||||
Thunix offers webmail at [https://thunix.net/webmail](/webmail). You can also connect via imap. Thunderbird will autodetect your settings, and we recommend the use of Thunderbird email client, so you can get the best-of-breed email experience. We also recommend using enigmail with Thunderbird as well. You can get it from your distro's package manager, or from [the Thunderbird project's site](https://www.thunderbird.net/)
|
||||
|
||||
## SSH Fingerprints
|
||||
|
||||
256 SHA256:iuqEWXCqUNihqO7o4xLrJ05M+te09i3P+WrHZsjUdZY thunix.net (ECDSA)
|
||||
3072 SHA256:7gZXCqNUYBfrTQ8wYmLrD6rVc5zL5nYf2l5t8ZrLIBU thunix.net (RSA)
|
||||
256 SHA256:d+J8u5pmQ8sR1BM8/EpUiNAlL9by371utl1ncNgFG6A thunix.net (ED25519)
|
||||
|
||||
## Onion Service
|
||||
|
||||
Thunix's services are available as a onion site as well. Our onion address is thunixme5v4rnoby.onion.
|
||||
|
||||
## BZFlag Game Server
|
||||
|
||||
BZFlag runs on the standard port. If you have a specific map you want loaded, send an email to root, and we'll look at getting the map swapped out
|
||||
Thunix's websites are available as a onion site as well. (SSH access is currently not available over Tor)
|
||||
Our Tor network (The Onion Router) address is kbguajmip4jlr2k3vpscmvymtxqnft267ox2ij6pdrgb5jcvx3kctkyd.onion.
|
||||
|
||||
## IRC Chat
|
||||
|
||||
Thunix is part of the tilde.chat network. You can access chat via the terminal, with the 'chat' command, via your favorite email client at irc.tilde.chat/6697, or via a web chat interface located [here]().
|
||||
Thunix is part of the Newnet IRC network. You can access chat via the terminal, with the 'chat' command, via your favorite email client at irc.newnet.net/6697, or via a web chat interface located [](https://web.newnet.net/?join=thunix).
|
||||
|
||||
## FOSS Project Mirrors
|
||||
|
||||
## FOSS Project Mirrors
|
||||
|
||||
Thunix hosts mirrors for several FOSS projects. You can see the full mirror list in the sidebar link.
|
||||
|
||||
## Website and CMS hosting
|
||||
|
||||
Collaborative creation, modification and management of digital content.
|
||||
- LAMP (Apache HTTP server, MySQL database and PHP scripting language) is an open source software stack on Linux environment, available to host a variety of web sites, content management systems and applications.
|
||||
- Django web development framework.
|
||||
|
||||
<!-- Begin autogen content from /includes/server.php -->
|
||||
|
||||
@@ -14,6 +14,6 @@ If you have any questions or problems, feel free to contact us.
|
||||
<input type='submit'>
|
||||
</form>
|
||||
|
||||
If you don't have a public SSH key, don't worry! Check out [this guide to SSH keys](https://tilde.team/wiki/?page=ssh) and make sure that you only fill in your public SSH key here.
|
||||
If you don't have a public SSH key, don't worry! Check out [this guide to SSH keys](https://wiki.thunix.net/wiki/ssh) and make sure that you only fill in your public SSH key here.
|
||||
|
||||
Signing up implies that you agree with our [terms of service](/tos). If you haven't done so, please read it before you sign up.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Success!
|
||||
# Form Failure
|
||||
|
||||
You will hear back shortly from us!
|
||||
You didn't properly fill out the signup form. Please try again.
|
||||
|
||||
3
articles/success3.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Form Failure
|
||||
|
||||
This username is already registered, please choose another one.
|
||||
3
articles/success4.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Form Failure
|
||||
|
||||
Please check SSH public key format.
|
||||
@@ -2,36 +2,39 @@
|
||||
|
||||
Nothing is without its rules and regulations; thunix is no exception.
|
||||
|
||||
Below are the site's service terms. Everything in this page should be clear to everyone who wishes to use thunix's services; failure to abide by these terms can result in penalties such as service bans and may result in legal action against any offending users, depending on the severity of each case and any other individuals who may have been affected. Thunix may conduct investigations on any suspected violations, and we will cooperate with law enforcement agencies with their investigations. Be sure to read this page carefully and understand it.
|
||||
Below are the site's service terms. All points listed should be clear to anyone wishing to use thunix's services. Failure to abide by these terms can result in penalties, including service bans and potential legal action, depending on the severity of the violation. Thunix may investigate suspected violations and will cooperate fully with law enforcement agencies as necessary. Read and understand these terms carefully.
|
||||
|
||||
You are responsibe for checking your local mail account from time to
|
||||
time. This email address is the only one we retain, and as such, any
|
||||
warnings and notices regarding your account, or service status will be
|
||||
sent to this account.
|
||||
You are responsible for regularly checking your local mail account. This email address is our primary means of communication, and any warnings or notices regarding your account or service status will be sent there.
|
||||
|
||||
1. Deliberately defacing the accounts of other users
|
||||
2. Deliberately trying to disrupt thunix's server
|
||||
3. Using thunix as a launch pad for disrupting other servers
|
||||
4. Using thunix to impersonate other websites and businesses for
|
||||
criminal purposes
|
||||
5. Storing/distributing pornography of any genre and medium (especially
|
||||
child pornography)
|
||||
6. Storing/distributing content that defames any individual
|
||||
7. Promoting racial, ethnic, religious, political and other forms of
|
||||
bigotry
|
||||
8. Storing/distributing, promoting, or encouraging use of
|
||||
pirated/cracked software, license keys, license/registration
|
||||
circumvention programs, or any questionable programs/scripts that
|
||||
can risk causing unauthorized modifications, or disrupts network
|
||||
services for any programs or devices.
|
||||
9. Mining cryptocurrencies
|
||||
10. Spamming on any forums, mailing lists, irc channels,
|
||||
newsgroups, etc.
|
||||
11. Leaking or publishing any user's or individual's personal
|
||||
information without that person's consent.
|
||||
Prohibited activities include, but are not limited to:
|
||||
|
||||
Copyright infringement is not allowed on thunix, and we will not allow any illegal content to be distributed. It's also expected that all users respect the copyrights of those who produce original content of any kind and only share if the author or license grants you permission. Content hosted on accounts used for external file storage must also follow copyright law.
|
||||
1. Deliberately defacing or tampering with other users' accounts.
|
||||
2. Intentionally disrupting or attempting to disrupt thunix's servers.
|
||||
3. Using thunix as a platform to attack or disrupt other servers or services.
|
||||
4. Using thunix to impersonate websites, individuals, or businesses for malicious or criminal purposes.
|
||||
5. Hosting or distributing pornography of any type or medium, especially child pornography.
|
||||
6. Storing, distributing, or promoting content that defames or slanders any individual or entity.
|
||||
7. Promoting racial, ethnic, religious, political, or other forms of bigotry or hate speech.
|
||||
8. Storing, distributing, promoting, or encouraging the use of pirated or cracked software, unauthorized license keys, or any software or scripts intended to bypass security or disrupt network services.
|
||||
9. Mining cryptocurrencies.
|
||||
10. Sending spam or unsolicited messages through forums, mailing lists, IRC channels, newsgroups, or other communication channels.
|
||||
11. Leaking, publishing, or distributing personal or sensitive information about any individual without explicit consent.
|
||||
12. Using thunix services for any activities you believe or suspect might be illegal.
|
||||
13. Using thunix services for commercial purposes or profit-making activities.
|
||||
14. Running servers or network services (such as web, mail, game, file-sharing, VPN, or proxy servers) without explicit permission.
|
||||
15. Reselling or subletting access to thunix services under any circumstances.
|
||||
16. Allowing or contributing to frequent attacks or disruptions targeting your account or hosted content. If such activities occur regularly, we reserve the right to disable or remove your service.
|
||||
|
||||
If you want to request for content you own the copyright for to be removed from thunix, please refer to our [abuse reporting page](/contact). In the email, please identify who you are, if you are the copyright holder or legally representing them, the exact files you want removed with links to the files and infringed content, and contact information such as phone numbers or a reply-to email address.
|
||||
### Additional Conditions:
|
||||
|
||||
- Thunix backs up your data on a best-effort basis, but users are responsible for maintaining their own regular backups.
|
||||
- Support provided by thunix is volunteer-based; mutual respect and patience are expected at all times.
|
||||
|
||||
Copyright infringement and distribution of illegal content are strictly prohibited. Users must respect copyright laws and obtain explicit permission from content owners or valid license holders before sharing any material. Content hosted for external storage purposes must also comply with all relevant copyright laws.
|
||||
|
||||
To request the removal of copyrighted material you own from thunix, please use our [abuse reporting page](/contact). Clearly identify your relationship to the copyrighted content, specify the exact files or links involved, and provide detailed contact information, including a reply-to email address or phone number.
|
||||
|
||||
Repeated or severe violations of these Terms of Service may result in immediate account removal. These terms apply to all communication platforms hosted by or associated with thunix, including forums, mailing lists, IRC channels, newsgroups, and similar services.
|
||||
|
||||
If you notice any violations, please report them through our [abuse reporting page](/contact).
|
||||
|
||||
Users that repeatedly violate the Terms of Service will have their account removed. Depending on the degree of the offense, their account may be removed immediately. These terms also apply to communication services such as forums, mailing lists, irc channels, newsgroups, and any other service either hosted on or used for thunix. If you come across anything that violates the terms of service, please let us know with the [abuse reporting page](/contact).
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<?php
|
||||
//Name of your site
|
||||
$site_name="☣ thunix ☣";
|
||||
$site_name="🌻 thunix 🌻";
|
||||
|
||||
//Root for the site, in a browser
|
||||
//$site_root="https://dev.thunix.cf";
|
||||
$site_root="https://".$_SERVER['HTTP_HOST'];
|
||||
$site_root="//".$_SERVER['HTTP_HOST'];
|
||||
//Local base root for app files
|
||||
$doc_root="/var/www/dev.thunix.cf";
|
||||
$doc_root="/var/www/thunix.cf";
|
||||
|
||||
//Site style
|
||||
// site is the default. Specify something else here to switch
|
||||
$site_style="thunix";
|
||||
$site_style="newthunix";
|
||||
?>
|
||||
|
||||
46
gen_tdp
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess,json,os.path,requests
|
||||
from bs4 import BeautifulSoup as bs
|
||||
|
||||
headers = {
|
||||
'Accept-Encoding': 'gzip, deflate, sdch',
|
||||
'Accept-Language': 'en-US,en;q=0.8',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Cache-Control': 'max-age=0',
|
||||
'Connection': 'keep-alive',
|
||||
}
|
||||
|
||||
tdp = {}
|
||||
tdp["name"] = "thunix"
|
||||
tdp["url"] = "https://www.thunix.net"
|
||||
tdp["signup_url"] = tdp["url"]+"/signup.php"
|
||||
users = subprocess.check_output(["/usr/bin/members","tilde"]).decode('ascii').split()
|
||||
users.sort()
|
||||
tdp["user_count"] = len(users)-1
|
||||
tdp["want_users"] = True
|
||||
tdp["admin_email"] = "root@thunix.net"
|
||||
tdp["description"] = "Thunix is a community, centered around access to a public *nix system. Thunix offers shell accounts with complete set of programming tools, and follows a continuous integration-continuous deployment of system configuration."
|
||||
tdpusers = []
|
||||
for user in users:
|
||||
if user=="anton":
|
||||
continue
|
||||
tdpuser = dict(username=user)
|
||||
title = bs(requests.get("https://www.thunix.net/~{}".format(user)).text,"lxml").title
|
||||
if title is None:
|
||||
title = "No title"
|
||||
else:
|
||||
title = title.text
|
||||
tdpuser["title"] = title
|
||||
if os.path.exists(os.path.expanduser("~{}/public_html/index.html".format(user))):
|
||||
tdpuser["mtime"] = os.path.getmtime(os.path.expanduser("~{}/public_html/index.html".format(user)))
|
||||
elif os.path.exists(os.path.expanduser("~{}/public_html/index.htm".format(user))):
|
||||
tdpuser["mtime"] = os.path.getmtime(os.path.expanduser("~{}/public_html/index.htm".format(user)))
|
||||
elif os.path.exists(os.path.expanduser("~{}/public_html/index.php".format(user))):
|
||||
tdpuser["mtime"] = os.path.getmtime(os.path.expanduser("~{}/public_html/index.php".format(user)))
|
||||
tdpusers.append(tdpuser)
|
||||
tdp["users"] = tdpusers
|
||||
|
||||
with open("tilde.json","w") as f:
|
||||
json.dump(tdp,f)
|
||||
43
githook.php
@@ -16,8 +16,6 @@
|
||||
|
||||
/* security */
|
||||
$access_token = "secret";
|
||||
$ansible_lastrun = '/dev/shm/ansible-hook-last-run';
|
||||
$ansible_dropfile = '/dev/shm/run-ansible';
|
||||
$www_lastrun = '/dev/shm/www-hook-last-run';
|
||||
$www_dropfile = '/dev/shm/run-www';
|
||||
$gopher_lastrun = '/dev/shm/gopher-hook-last-run';
|
||||
@@ -25,7 +23,7 @@ $gopher_dropfile = '/dev/shm/run-gopher';
|
||||
$wiki_lastrun = '/dev/shm/wiki-hook-last-run';
|
||||
$wiki_dropfile = '/dev/shm/run-wiki';
|
||||
|
||||
$allowedip = '51.79.32.48';
|
||||
$allowedip = '198.50.210.248';
|
||||
$remoteip = $_SERVER['REMOTE_ADDR'];
|
||||
$ratelimit = 300;
|
||||
|
||||
@@ -51,24 +49,6 @@ if ( strcmp($remoteip, $allowedip) !== 0 )
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Hook for ansible here
|
||||
if ($data["repository"]["full_name"] == 'thunix/ansible') {
|
||||
syslog(LOG_INFO, 'Ansible Webhook recieved.');
|
||||
// We limit runs to once per 5 minutes, so they don't try
|
||||
// overlapping. Systemd shouldn't allow it, but we'll check
|
||||
// anyways
|
||||
if ( time () - filemtime ( $ansible_lastrun ) > $ratelimit ) {
|
||||
touch ( $ansible_dropfile );
|
||||
touch ( $ansible_lastrun );
|
||||
echo "HTTP 200 - Ansible webhook recieved.\n";
|
||||
}
|
||||
else {
|
||||
http_response_code(429);
|
||||
echo "HTTP 429 - Rate Limited.\n";
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Hook for www repo here. Same rules apply, as above, for www. We
|
||||
// could probably make it able to run more frequently. Backend job is
|
||||
// just a git pull, and is quick.
|
||||
@@ -119,8 +99,8 @@ elseif ($data["repository"]["full_name"] == 'thunix/wiki') {
|
||||
}
|
||||
}
|
||||
|
||||
// Easter egg for anyone probing the hook. Enjoy. We're a coffee maker
|
||||
// and not a teapot :)
|
||||
// Easter egg for anyone probing the hook. Enjoy. We're a tea pot
|
||||
// and not a coffee maker :)
|
||||
else {
|
||||
http_response_code(418);
|
||||
echo "HTTP 418 - I'm a teapot.\n";
|
||||
@@ -128,22 +108,5 @@ else {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/*$fp = pfsockopen( "tcp://127.0.0.1", 1234, $errno, $errstr );
|
||||
|
||||
if (!$fp)
|
||||
{
|
||||
echo "ERROR: $errno - $errstr<br />\n";
|
||||
}
|
||||
socket_set_timeout ($fp, 10);
|
||||
$msg = "Commit '".$data['commits'][0]["message"]."' was pushed to ".$data["repository"]["full_name"].' by '.$data["pusher"]["login"];
|
||||
$msg = trim(preg_replace('/\s+/', ' ', $msg));
|
||||
$write = fwrite ($fp, $msg);
|
||||
fclose($fp);
|
||||
|
||||
if (!$write) {
|
||||
echo "error writing to port.<br/>";
|
||||
next;
|
||||
}
|
||||
*/
|
||||
?>
|
||||
|
||||
|
||||
10
humans.txt
@@ -1,16 +1,16 @@
|
||||
/* TEAM */
|
||||
Your title: ubergeek, naglfar, amcclure, fosslinux
|
||||
Your title: deepend, naglfar
|
||||
Site: https://thunix.net, root@thunix.net
|
||||
Location: Frankfurt, Germany
|
||||
Location: Quebec, Canada
|
||||
|
||||
/* THANKS */
|
||||
Name: ubergeek https://thunix.net/~ubergeek
|
||||
amcclure https://thunix.net/~amcclure
|
||||
Name: deepend https://thunix.net/~deepend
|
||||
ubergeek https://thunix.net/~ubergeek
|
||||
naglfar https://thunix.net/~naglfar
|
||||
fosslinux https://thunix.net/~fosslinux
|
||||
|
||||
/* SITE */
|
||||
Last update: 2019/03/22
|
||||
Last update: 2023/12/26
|
||||
Standards: HTML5
|
||||
Components: Apache2, Git, and PHP, Parsedown, Parsedown Extra
|
||||
Software: vim, geany, ansible, wiki.php
|
||||
|
||||
BIN
images/mail.png
|
Before Width: | Height: | Size: 650 KiB |
@@ -2,8 +2,14 @@
|
||||
include "../config.php";
|
||||
// This code is licensed under the AGPL 3 or later by ubergeek (https://tildegit.org/ubergeek)
|
||||
|
||||
// Optional: keep the terminal UI flow inside /terminal/ without changing the classic site.
|
||||
//
|
||||
// Prefer an explicit flag (terminal=1). As a fallback, detect a terminal embed
|
||||
// by referrer so the classic site behavior stays unchanged.
|
||||
$terminalMode = (isset($_REQUEST['terminal']) && (string) $_REQUEST['terminal'] === '1')
|
||||
|| (isset($_SERVER['HTTP_REFERER']) && strpos((string) $_SERVER['HTTP_REFERER'], '/terminal/') !== false);
|
||||
$name = $_GET['contact_name'];
|
||||
$email = $_GET['email_address'];
|
||||
$return_addr = $_GET['email_address'];
|
||||
$type = $_GET['type'];
|
||||
$body = $_GET['message'];
|
||||
|
||||
@@ -14,22 +20,27 @@ $subject = "Contact Form";
|
||||
$mailbody = "The following submission via the contact form was recieved:
|
||||
|
||||
Real Name: $name
|
||||
Email Address: $email
|
||||
Type: $type
|
||||
Message: $body";
|
||||
|
||||
if ( $tv != "tildeverse" ) {
|
||||
print "Spam attempt";
|
||||
header("Location: $site_root/?page=success1");
|
||||
$redirect = $terminalMode
|
||||
? $site_root . "/terminal/view.php?page=success1"
|
||||
: $site_root . "/?page=success1";
|
||||
header("Location: $redirect");
|
||||
die();
|
||||
}
|
||||
|
||||
shell_exec("echo '$mailbody' | /usr/bin/mail -s '$subject' $destination_addr ");
|
||||
shell_exec("echo '$mailbody' | /usr/bin/mail -s '$subject' -r '$return_addr' $destination_addr ");
|
||||
|
||||
// In the future, here, we *should* be able to build a process that
|
||||
// auto opens an issue in the tildegit project
|
||||
|
||||
header("Location: $site_root/?page=success2");
|
||||
$redirect = $terminalMode
|
||||
? $site_root . "/terminal/view.php?page=success2"
|
||||
: $site_root . "/?page=success2";
|
||||
header("Location: $redirect");
|
||||
die()
|
||||
|
||||
?>
|
||||
|
||||
BIN
includes/dot.ttf
Executable file
@@ -1,7 +1,5 @@
|
||||
Unless otherwise noted, all thunix.net materials (besides all user-generated content) is licensed as [CC BY-SA 4.0license](https://creativecommons.org/licenses/by-sa/4.0/). Permissions beyond the scope of this license may be available [here](/copyright).
|
||||
|
||||
Users define their own licensing and own all of their content.
|
||||
All content is licensed as [CC BY-SA 4.0 license](https://creativecommons.org/licenses/by-sa/4.0/). Users define their own licensing and own all of their content.
|
||||
|
||||
All questions, comments, and concerns about this site should be sent to [the administration team](/contact).
|
||||
|
||||
Created with valid [HTML](https://validator.w3.org/check?uri=referer) and [CSS](https://jigsaw.w3.org/css-validator/check/referer) code.
|
||||
Created with valid [HTML](https://validator.w3.org/check?uri=referer) and [CSS](https://jigsaw.w3.org/css-validator/check/referer) code.
|
||||
@@ -1 +1 @@
|
||||

|
||||

|
||||
|
||||
147
includes/newthunix.css
Normal file
@@ -0,0 +1,147 @@
|
||||
@font-face { font-family: dot; src: url('https://thunix.net/includes/dot.ttf'); }
|
||||
body {
|
||||
font-family: "dot", Courier, monospace;
|
||||
background: #000;
|
||||
color: #F79862;
|
||||
}
|
||||
#body {
|
||||
width: 95%;
|
||||
}
|
||||
#body h1, #body h2, #body h3 {
|
||||
color: orange;
|
||||
}
|
||||
a {
|
||||
background: #000;
|
||||
color: #F79862;
|
||||
}
|
||||
a:visited {
|
||||
color: orange;
|
||||
}
|
||||
#header {
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
font-size: xx-large;
|
||||
}
|
||||
#content {
|
||||
width: 78%;
|
||||
float: left;
|
||||
font-size: medium;
|
||||
}
|
||||
#content img {
|
||||
max-height: 400px;
|
||||
max-width: 98%;
|
||||
}
|
||||
.lineitem {
|
||||
border: 1px;
|
||||
border-color: #fff;
|
||||
}
|
||||
#sidebar {
|
||||
width: 18%;
|
||||
float: right;
|
||||
font-size: small;
|
||||
padding: 15px; /* Increase padding for more internal spacing */
|
||||
margin-top: 20px; /* Add more space between the sidebar and elements above it */
|
||||
margin-left: 10px; /* Add space between the sidebar and content */
|
||||
background-color: #111; /* Ensure the sidebar stands out slightly */
|
||||
line-height: 1.5; /* Increase line height for better readability */
|
||||
border: 1px solid #333; /* Optional: Add a subtle border to separate the sidebar visually */
|
||||
}
|
||||
#sidebar h1, #sidebar h2 {
|
||||
color: orange;
|
||||
/* background: #000; */
|
||||
margin-bottom: 15px; /* Further space below headings */
|
||||
padding-bottom: 5px; /* Add padding under headings for better distinction */
|
||||
border-bottom: 1px solid #333; /* Optional: Add an underline effect for headings */
|
||||
}
|
||||
#sidebar ul, #sidebar p {
|
||||
margin: 10px 0; /* Add vertical spacing between sidebar elements */
|
||||
padding-left: 20px; /* Indent list items for better clarity */
|
||||
}
|
||||
|
||||
#footer {
|
||||
width: 95%;
|
||||
text-align: center;
|
||||
clear: both;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
#new
|
||||
|
||||
/* 1) Keep terminal feel, but make reading humane */
|
||||
body {
|
||||
background: #050505; /* not pure black */
|
||||
color: #f1a67a; /* slightly softer than #F79862 */
|
||||
font-size: 16px; /* consistent baseline */
|
||||
line-height: 1.65; /* biggest readability win */
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* 2) Use the dot font for headings/branding, but not for paragraphs */
|
||||
#header,
|
||||
#body h1, #body h2, #body h3 {
|
||||
font-family: "dot", Courier, monospace;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
#content,
|
||||
#sidebar,
|
||||
#footer {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* still terminal, but readable */
|
||||
}
|
||||
|
||||
/* 3) Limit line length so the eyes don't have to travel across Alberta */
|
||||
#content {
|
||||
max-width: 980px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 4) Make links look like links, not “random orange words” */
|
||||
a {
|
||||
color: #f1a67a;
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #ffb58f;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #e6a042; /* still orange, but distinct */
|
||||
}
|
||||
|
||||
/* 5) Softer headings and better spacing */
|
||||
#body h1, #body h2, #body h3,
|
||||
#sidebar h1, #sidebar h2 {
|
||||
color: #ffb347; /* softer orange */
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
|
||||
/* 6) Sidebar: keep the panel look, make it calmer */
|
||||
#sidebar {
|
||||
background-color: #0c0c0c; /* slightly lighter */
|
||||
border: 1px solid #222;
|
||||
border-radius: 6px; /* tiny, still “retro UI” */
|
||||
}
|
||||
|
||||
/* 7) Fix your lineitem border (currently border:1px does nothing) */
|
||||
.lineitem {
|
||||
border: 1px solid #222;
|
||||
}
|
||||
|
||||
/* 8) Optional: code blocks that actually read nicely */
|
||||
pre, code {
|
||||
background: #0b0b0b;
|
||||
border: 1px solid #1f1f1f;
|
||||
border-radius: 6px;
|
||||
padding: 0.15em 0.35em;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -1,77 +1,9 @@
|
||||
<?php
|
||||
/*
|
||||
This code is licensed under the AGPL 3 or later by ubergeek (https://tildegit.org/ubergeek)
|
||||
Parsedown is licensed under the MIT license.
|
||||
*/
|
||||
|
||||
include('../config.php');
|
||||
include('../parsedown-1.7.3/Parsedown.php');
|
||||
include('../parsedown-extra-0.7.1/ParsedownExtra.php');
|
||||
|
||||
$page = $_GET['page'];
|
||||
$style = $_GET['style'];
|
||||
$Parsedown = new Parsedown();
|
||||
$Parsedown->setMarkupEscaped(true);
|
||||
$ParsedownExtra = new ParsedownExtra();
|
||||
|
||||
if ( $page == "") {
|
||||
$page = "main";
|
||||
}
|
||||
|
||||
if ( $style == "") {
|
||||
if ( $site_style == "") {
|
||||
$site_style="site";
|
||||
}
|
||||
}
|
||||
else {
|
||||
$site_style=$style;
|
||||
}
|
||||
|
||||
$header = file_get_contents("$doc_root/includes/header.md");
|
||||
$sidebar = file_get_contents("$doc_root/includes/sidebar.md");
|
||||
$content = file_get_contents("$doc_root/articles/server.md");
|
||||
$footer = file_get_contents("$doc_root/includes/footer.md");
|
||||
|
||||
print "<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<title>$site_name - $page</title>
|
||||
<link rel='stylesheet' type='text/css' href='$site_root/includes/$site_style.css'>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Begin Header -->
|
||||
|
||||
<div id='header'>";
|
||||
|
||||
print $Parsedown->text($header);
|
||||
|
||||
print "
|
||||
</div>
|
||||
<!-- End Header -->
|
||||
";
|
||||
|
||||
print "<hr>
|
||||
<div id='body'>
|
||||
|
||||
<!-- Begin Sidebar -->
|
||||
<div id='sidebar'>
|
||||
";
|
||||
|
||||
echo $Parsedown->text($sidebar);
|
||||
|
||||
print " </div>
|
||||
<!-- End Sidebar -->
|
||||
|
||||
<!-- Begin Body -->
|
||||
<div id='content'>";
|
||||
|
||||
echo $ParsedownExtra->text($content);
|
||||
|
||||
// Monitoring section
|
||||
|
||||
$hosts="all";
|
||||
|
||||
$f = fopen("./report", "r");
|
||||
$f = fopen("$doc_root/report", "r");
|
||||
|
||||
echo "Last update: " . date ("H:i", filemtime('./report'))."<p>\n";
|
||||
echo "<table style='width:80%'>";
|
||||
@@ -113,21 +45,4 @@ echo "\n</table>\n";
|
||||
fclose($f);
|
||||
|
||||
// End monitoring section
|
||||
print " </div>
|
||||
<!-- End Body -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Begin Footer -->
|
||||
<div id='footer'>
|
||||
<hr>
|
||||
";
|
||||
|
||||
echo $Parsedown->text($footer);
|
||||
|
||||
print " </div>
|
||||
<!-- End Footer -->
|
||||
|
||||
</body>
|
||||
</html>";
|
||||
?>
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
- Main Menu
|
||||
---------
|
||||
|
||||
- [Home](/main)
|
||||
- [Sign Up](/signup)
|
||||
- [FAQ](/faq)
|
||||
- [Terms of Service](/tos)
|
||||
- [GDPR Statement and Privacy Policy](/gdpr)
|
||||
- [Privacy Policy](/privacy)
|
||||
- [Contact Us](/contact)
|
||||
- [Sign Up](/signup)
|
||||
- [thunix Mirror Services](https://ftp.thunix.net/)
|
||||
- [Web Server Stats](https://stats.thunix.net/)
|
||||
- [Donations](/donate)
|
||||
- Main Resources On This Site
|
||||
---------------------------
|
||||
- [Donations](https://donate.tilde.club)
|
||||
|
||||
- Resources and User Content
|
||||
---------------------------
|
||||
- [Wiki](https://wiki.thunix.net/)
|
||||
- [Status of thunix Servers and Services](/server)
|
||||
- [Service News](/news)
|
||||
- [User Web Directories](/users)
|
||||
- [User Gopher Directories](https://gopher.tildeverse.org/thunix.net)
|
||||
- [Web Mail](/webmail/)
|
||||
- [ZNC Service](https://thunix.net:1326/)
|
||||
- Server Staff
|
||||
------------
|
||||
- [User Gemini Directories](https://gemini.tildeverse.org/?gemini://thunix.net/)
|
||||
|
||||
- [Anton McClure](/~amcclure/)
|
||||
- [Ubergeek](/~ubergeek/)
|
||||
- Services and Status
|
||||
--------------------
|
||||
- [Status and Information](/server)
|
||||
- [Service News](/news)
|
||||
- [Web Server Stats](https://stats.thunix.net/)
|
||||
- [Web Mail](/webmail/)
|
||||
- [ZNC Service](https://thunix.net:1356/)
|
||||
|
||||
- Server Staff
|
||||
-------------
|
||||
- [deepend](/~deepend/)
|
||||
- [Naglfar](/~naglfar/)
|
||||
- [fosslinux](/~fosslinux/)
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
<?php
|
||||
// This code is licensed under the AGPL 3 or later by ubergeek (https://tildegit.org/ubergeek)
|
||||
include "../config.php";
|
||||
|
||||
$name = $_GET['contact_name'];
|
||||
$email = $_GET['email_address'];
|
||||
$username = $_GET['username'];
|
||||
$interest = $_GET['interest'];
|
||||
$pubkey = $_GET['pubkey'];
|
||||
$tv = $_GET['tv'];
|
||||
// Optional: keep the terminal UI flow inside /terminal/ without changing the classic site.
|
||||
//
|
||||
// Prefer an explicit flag (terminal=1). As a fallback, detect a terminal embed
|
||||
// by referrer so the classic site behavior stays unchanged.
|
||||
$terminalMode = (isset($_REQUEST['terminal']) && (string) $_REQUEST['terminal'] === '1')
|
||||
|| (isset($_SERVER['HTTP_REFERER']) && strpos((string) $_SERVER['HTTP_REFERER'], '/terminal/') !== false);
|
||||
$name = $_GET['contact_name'];
|
||||
$email = $_GET['email_address'];
|
||||
$username = $_GET['username'];
|
||||
$interest = $_GET['interest'];
|
||||
$pubkey = $_GET['pubkey'];
|
||||
$tv = $_GET['tv'];
|
||||
|
||||
$destination_addr = "newuser@thunix.net";
|
||||
$subject = "New User Registration";
|
||||
$username = strtolower($username);
|
||||
$pubkey = trim($pubkey);
|
||||
|
||||
$from = 'From: www-data <www-data@thunix.net>';
|
||||
$destination_addr = 'newuser@thunix.net';
|
||||
$subject = 'New User Registration';
|
||||
$mailbody = "A new user has tried to register.
|
||||
Username: $username
|
||||
Real Name: $name
|
||||
@@ -18,19 +27,33 @@ Email Address: $email
|
||||
Interest: $interest
|
||||
Pubkey: $pubkey";
|
||||
|
||||
if ( $tv != "tildeverse" ) {
|
||||
print "Spam attempt";
|
||||
header("Location: $site_root/?page=success1");
|
||||
die();
|
||||
$user_queue = '/dev/shm/userqueue';
|
||||
|
||||
$success = 'success1';
|
||||
if ($tv == 'tildeverse') {
|
||||
$success = 'success2';
|
||||
if (posix_getpwnam($username)) {
|
||||
$success = 'success3';
|
||||
}
|
||||
$valid_key_starts = ['ssh-rsa', 'ssh-dss', 'ecdsa-sha2', 'ssh-ed25519'];
|
||||
$key_parts = explode(' ', $pubkey, 3);
|
||||
if (!in_array($key_parts[0], $valid_key_starts) || count($key_parts) < 2) {
|
||||
$success = 'success4';
|
||||
}
|
||||
if ($success === 'success2') {
|
||||
mail($destination_addr, $subject, $mailbody, $from);
|
||||
$fp = fopen($user_queue, 'a');
|
||||
fwrite($fp, "'$username','$email','$pubkey'\n");
|
||||
fclose($fp);
|
||||
$fp2 = fopen('/var/signups', 'a');
|
||||
fwrite($fp2, 'makeuser ' . $username . ' ' . $email . ' "' . addslashes($pubkey) . "\"\n");
|
||||
fclose($fp2);
|
||||
}
|
||||
}
|
||||
|
||||
shell_exec("echo '$mailbody' | /usr/bin/mail -s '$subject' $destination_addr ");
|
||||
|
||||
// In the future, here, we *should* be able to build a process that
|
||||
// somehow auto-verifies the user, and instead of email, it'll kick off the new user process here
|
||||
|
||||
header("Location: $site_root/?page=success2");
|
||||
|
||||
if ($terminalMode) {
|
||||
header("Location: $site_root/terminal/view.php?page=$success");
|
||||
} else {
|
||||
header("Location: $site_root/?page=$success");
|
||||
}
|
||||
die();
|
||||
|
||||
?>
|
||||
|
||||
265
includes/terminal.css
Normal file
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
Terminal theme for /terminal/view.php.
|
||||
Keeps the same wiki.php content pipeline (Parsedown/ParsedownExtra),
|
||||
but makes it look like the amber CRT terminal UI.
|
||||
|
||||
This file is loaded inside an <iframe> (terminal content panel), so it must
|
||||
style full documents reliably without depending on any outer page CSS.
|
||||
*/
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
/* Keep these in sync with terminal/index.php. */
|
||||
--mono: "Departure Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New", monospace;
|
||||
--bg: #111111;
|
||||
--panel: rgba(255, 255, 255, 0.03);
|
||||
--amber: #edb200;
|
||||
--amber-txt: #ffc828;
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
--glow: rgba(237, 178, 0, 0.55);
|
||||
--glow-soft: rgba(237, 178, 0, 0.25);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--amber);
|
||||
font-family: var(--mono);
|
||||
font-size: 18px; /* Match xterm.js default in terminal.js */
|
||||
line-height: 1.55;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* CRT-ish scanlines, because humans love pretending it’s 1996. */
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.28) 51%);
|
||||
background-size: 100% 4px;
|
||||
}
|
||||
|
||||
/* Keep content above page background but below scanlines. */
|
||||
#content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
padding: 18px 20px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Hide classic wiki chrome if a fragment includes it for any reason. */
|
||||
#header,
|
||||
#sidebar,
|
||||
#footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Typographic polish */
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin: 0.9em 0 0.35em;
|
||||
line-height: 1.15;
|
||||
color: var(--amber-txt);
|
||||
text-shadow: 0 0 1.4rem var(--glow-soft);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.55rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--amber-txt);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
/* Keep images sane inside the panel. */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul,
|
||||
ol {
|
||||
margin: 0.65em 0 0.9em 1.25em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a,
|
||||
a:visited {
|
||||
color: var(--amber-txt);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 3px;
|
||||
text-shadow: 0 0 0.85rem rgba(237, 178, 0, 0.3);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
filter: brightness(1.08);
|
||||
text-shadow: 0 0 1.25rem rgba(237, 178, 0, 0.55);
|
||||
}
|
||||
|
||||
/* Code */
|
||||
code,
|
||||
pre {
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
code {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 0.08em 0.3em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
overflow: auto;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Tables (contact/signup forms, lists). */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
tr + tr td,
|
||||
tr + tr th {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
/* Form controls should look like they belong here. */
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background: var(--panel);
|
||||
color: var(--amber);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
font-family: var(--mono);
|
||||
font-size: 18px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
border-color: rgba(255, 200, 40, 0.55);
|
||||
box-shadow: 0 0 0 2px rgba(255, 200, 40, 0.15);
|
||||
}
|
||||
|
||||
input[type='submit'],
|
||||
button {
|
||||
width: auto;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 200, 40, 0.14);
|
||||
border-color: rgba(255, 200, 40, 0.32);
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
input[type='submit']:hover,
|
||||
button:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
blockquote {
|
||||
margin: 1em 0;
|
||||
padding: 0.2em 0.9em;
|
||||
border-left: 3px solid rgba(255, 200, 40, 0.28);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
::selection {
|
||||
background: rgba(255, 200, 40, 0.22);
|
||||
color: var(--amber-txt);
|
||||
}
|
||||
|
||||
/* Scrollbars (best-effort, doesn’t break anything if unsupported). */
|
||||
html {
|
||||
scrollbar-color: rgba(255, 200, 40, 0.55) rgba(0, 0, 0, 0.2);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 200, 40, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 200, 40, 0.5);
|
||||
}
|
||||
@@ -1,98 +1,26 @@
|
||||
<?php
|
||||
/*
|
||||
This code is licensed under the AGPL 3 or later by ubergeek (https://tildegit.org/ubergeek)
|
||||
Parsedown and Parsedown Extra is licensed under the MIT license.
|
||||
*/
|
||||
|
||||
include('../config.php');
|
||||
include('../parsedown-1.7.3/Parsedown.php');
|
||||
include('../parsedown-extra-0.7.1/ParsedownExtra.php');
|
||||
|
||||
$page = $_GET['page'];
|
||||
$style = $_GET['style'];
|
||||
$Parsedown = new Parsedown();
|
||||
$Parsedown->setMarkupEscaped(true);
|
||||
$ParsedownExtra = new ParsedownExtra();
|
||||
|
||||
if ( $page == "") {
|
||||
$page = "main";
|
||||
}
|
||||
|
||||
if ( $style == "") {
|
||||
if ( $site_style == "") {
|
||||
$site_style="site";
|
||||
}
|
||||
}
|
||||
else {
|
||||
$site_style=$style;
|
||||
}
|
||||
|
||||
$header = file_get_contents("$doc_root/includes/header.md");
|
||||
$sidebar = file_get_contents("$doc_root/includes/sidebar.md");
|
||||
$content = file_get_contents("$doc_root/articles/userdir.md");
|
||||
$footer = file_get_contents("$doc_root/includes/footer.md");
|
||||
|
||||
print "<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<title>$site_name - $page</title>
|
||||
<link rel='stylesheet' type='text/css' href='$site_root/includes/$site_style.css'>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Begin Header -->
|
||||
|
||||
<div id='header'>";
|
||||
|
||||
print $Parsedown->text($header);
|
||||
|
||||
print "
|
||||
</div>
|
||||
<!-- End Header -->
|
||||
";
|
||||
|
||||
print "<hr>
|
||||
<div id='body'>
|
||||
|
||||
<!-- Begin Sidebar -->
|
||||
<div id='sidebar'>
|
||||
";
|
||||
|
||||
echo $Parsedown->text($sidebar);
|
||||
|
||||
print " </div>
|
||||
<!-- End Sidebar -->
|
||||
|
||||
<!-- Begin Body -->
|
||||
<div id='content'>";
|
||||
|
||||
echo $ParsedownExtra->text($content);
|
||||
$html_skel = '/etc/skel/public_html/index.html';
|
||||
|
||||
print "<!-- Begin autogen userdir list -->";
|
||||
print "<ul style='list-style: none; margin-left: -40px;'>";
|
||||
foreach (glob("/home/*") as $user):
|
||||
if (!is_dir($user . "/public_html") || (!file_exists($user . "/public_html/index.html") && !file_exists($user . "/public_html/index.php")))
|
||||
continue;
|
||||
$user = basename($user);
|
||||
print"<li><a href='$site_root/~$user/'>~$user</a></li>";
|
||||
endforeach;
|
||||
print "</ul></div>
|
||||
<!-- End Autgen userdir list -->";
|
||||
|
||||
print " </div>
|
||||
<!-- End Body -->
|
||||
foreach (glob("/home/*") as $userpath) {
|
||||
if (is_dir("$userpath/public_html")) {
|
||||
$user = basename($userpath);
|
||||
|
||||
</div>
|
||||
// Use @ to suppress warnings in case the user directory/files are not readable.
|
||||
$skeletonMatch = (@sha1_file($html_skel) === @sha1_file("$userpath/public_html/index.html"));
|
||||
$isEmptyPubhtml = (@count(@scandir("$userpath/public_html")) === 2); // 2 => "." and ".."
|
||||
|
||||
<!-- Begin Footer -->
|
||||
<div id='footer'>
|
||||
<hr>
|
||||
";
|
||||
if ($skeletonMatch || $isEmptyPubhtml) {
|
||||
// If it matches the skeleton index.html or is empty, display without a link
|
||||
print "<li>~$user</li>\n";
|
||||
} else {
|
||||
// Otherwise, link to the user's directory
|
||||
print "<li><a href='$site_root/~$user/'>~$user</a></li>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo $Parsedown->text($footer);
|
||||
|
||||
print " </div>
|
||||
<!-- End Footer -->
|
||||
|
||||
</body>
|
||||
</html>";
|
||||
print "</ul></div>\n<!-- End Autgen userdir list -->";
|
||||
?>
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
BIN
media/mail.png
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 447 KiB |
BIN
media/thunix.gif
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
media/thunix.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
427
sitemap.xml
Normal file
@@ -0,0 +1,427 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset
|
||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
|
||||
|
||||
|
||||
<url>
|
||||
<loc>https://www.thunix.net/</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>1.00</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/main</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/faq</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/tos</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/gdpr</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/contact</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/signup</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/donate</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/server</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/news</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/users</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~amcclure/</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ubergeek/</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~naglfar/</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~fosslinux/</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/copyright</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~adam/</loc>
|
||||
<lastmod>2019-06-10T13:48:44+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~aniruddh/</loc>
|
||||
<lastmod>2019-04-24T17:25:18+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~brendantcc/</loc>
|
||||
<lastmod>2019-02-17T23:13:11+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~cyphyx/</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~diabla/</loc>
|
||||
<lastmod>2019-01-10T23:22:46+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~gokce/</loc>
|
||||
<lastmod>2019-01-07T22:00:54+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~isaac/</loc>
|
||||
<lastmod>2019-07-09T01:02:26+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~khuxkm/</loc>
|
||||
<lastmod>2019-06-14T13:18:42+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~mandlebroth/</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ml/</loc>
|
||||
<lastmod>2019-01-23T10:05:23+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~orliesaurus/</loc>
|
||||
<lastmod>2019-01-07T02:00:52+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~quetzalcoatl/</loc>
|
||||
<lastmod>2019-01-21T23:53:53+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ringo/</loc>
|
||||
<lastmod>2019-01-03T01:11:03+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~sancho/</loc>
|
||||
<lastmod>2019-06-01T22:07:12+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~smtpsupplicant/</loc>
|
||||
<lastmod>2019-05-18T18:47:11+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~techemporium/</loc>
|
||||
<lastmod>2019-06-15T23:44:56+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~thekingofbandit/</loc>
|
||||
<lastmod>2019-05-29T04:46:22+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~usernameak/</loc>
|
||||
<lastmod>2019-01-15T22:17:49+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~amcclure/home</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~amcclure/contact</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~amcclure/documents</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~amcclure/projects</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~amcclure/links</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~amcclure/readme</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~amcclure/license</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~amcclure/tos</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~amcclure/privacy</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~amcclure/etiquette</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ubergeek/main</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ubergeek/gopher</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ubergeek/workstation</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ubergeek/cli</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ubergeek/thunix</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ubergeek/sitecode</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~fosslinux/?C=N;O=D</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~fosslinux/?C=M;O=A</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~fosslinux/?C=S;O=A</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~fosslinux/?C=D;O=A</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~fosslinux/glowing-bear/</loc>
|
||||
<lastmod>2019-02-08T00:38:04+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~fosslinux/proxy.php</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.64</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~adam/copyright.html</loc>
|
||||
<lastmod>2019-06-10T13:44:58+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~brendantcc/.</loc>
|
||||
<lastmod>2019-02-17T23:13:11+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/en</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/en/archive</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ml/lynx_bookmarks.html</loc>
|
||||
<lastmod>2019-07-15T11:48:29+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ml/fwa.txt</loc>
|
||||
<lastmod>2019-07-16T15:57:08+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ml/tt.txt</loc>
|
||||
<lastmod>2019-01-21T07:45:06+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~ml/lw</loc>
|
||||
<lastmod>2019-07-15T11:48:29+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~sancho/.</loc>
|
||||
<lastmod>2019-06-01T22:07:12+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~sancho/pages/about-me.html</loc>
|
||||
<lastmod>2019-06-01T22:07:12+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~sancho/category/general.html</loc>
|
||||
<lastmod>2019-06-01T22:07:12+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~sancho/first-post.html</loc>
|
||||
<lastmod>2019-06-01T22:07:12+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/copyright.htm</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~techemporium/htpwdgen.php</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~techemporium/crapforum.php</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~techemporium/hcsfs.php</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~techemporium/pmcms.php</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~techemporium/sshacs.php</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~techemporium/warlord.php</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~techemporium/technes.php</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~techemporium/browzos.php</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~fosslinux/?C=N;O=A</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~fosslinux/?C=M;O=D</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~fosslinux/?C=S;O=D</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~fosslinux/?C=D;O=D</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.51</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~techemporium/gpl-3.0.php</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.41</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.thunix.net/~techemporium/mit_license.php</loc>
|
||||
<lastmod>2019-07-17T01:25:07+00:00</lastmod>
|
||||
<priority>0.41</priority>
|
||||
</url>
|
||||
|
||||
|
||||
</urlset>
|
||||
27
terminal/api/_bootstrap.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
$rootDir = dirname(__DIR__, 2);
|
||||
if (!is_dir($rootDir)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Invalid root directory'], JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
function json_out(array $data, int $status = 200): void
|
||||
{
|
||||
http_response_code($status);
|
||||
try {
|
||||
echo json_encode(
|
||||
$data,
|
||||
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo '{"error":"JSON encoding failed"}';
|
||||
}
|
||||
exit;
|
||||
}
|
||||
58
terminal/api/menu.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/terminal/_bootstrap.php';
|
||||
|
||||
$file = $rootDir . '/includes/sidebar.md';
|
||||
if (!is_file($file)) {
|
||||
json_out(['sections' => []]);
|
||||
}
|
||||
|
||||
$md = file_get_contents($file);
|
||||
if ($md === false) {
|
||||
json_out(['sections' => []]);
|
||||
}
|
||||
|
||||
$lines = preg_split('/\r\n|\n|\r/', $md) ?: [];
|
||||
$sections = [];
|
||||
|
||||
$current = null;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$raw = rtrim($line);
|
||||
|
||||
// Section header: "- Title"
|
||||
if (preg_match('/^\-\s{2,}(.+)$/', $raw, $m)) {
|
||||
if ($current !== null) {
|
||||
$sections[] = $current;
|
||||
}
|
||||
$current = [
|
||||
'title' => trim($m[1]),
|
||||
'items' => [],
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Menu item: " - [Text](Href)"
|
||||
if ($current !== null && preg_match('/^\s+\-\s{2,}\[(.+?)\]\((.+?)\)\s*$/', $raw, $m)) {
|
||||
$text = trim($m[1]);
|
||||
$href = trim($m[2]);
|
||||
|
||||
$internal = str_starts_with($href, '/');
|
||||
$slug = $internal ? ltrim(parse_url($href, PHP_URL_PATH) ?? '', '/') : '';
|
||||
|
||||
$current['items'][] = [
|
||||
'text' => $text,
|
||||
'href' => $href,
|
||||
'internal' => $internal,
|
||||
'slug' => $slug,
|
||||
];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($current !== null) {
|
||||
$sections[] = $current;
|
||||
}
|
||||
|
||||
json_out(['sections' => $sections]);
|
||||
53
terminal/api/page.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$p = $_GET['p'] ?? '';
|
||||
if (!is_string($p)) {
|
||||
json_out(['error' => 'Invalid page'], 400);
|
||||
}
|
||||
|
||||
$slug = trim($p);
|
||||
$slug = ltrim($slug, '/');
|
||||
$slug = preg_replace('/\?.*$/', '', $slug) ?? $slug;
|
||||
|
||||
if ($slug === '' || !preg_match('/^[A-Za-z0-9][A-Za-z0-9_-]*$/', $slug)) {
|
||||
json_out(['error' => 'Invalid page'], 400);
|
||||
}
|
||||
|
||||
if (preg_match('/^success\d+$/', $slug) === 1) {
|
||||
json_out(['error' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$file = $rootDir . '/articles/' . $slug . '.md';
|
||||
if (!is_file($file)) {
|
||||
json_out(['error' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$md = file_get_contents($file);
|
||||
if ($md === false) {
|
||||
json_out(['error' => 'Failed to read page'], 500);
|
||||
}
|
||||
|
||||
$parsedownPath = $rootDir . '/parsedown-1.7.3/Parsedown.php';
|
||||
$extraPath = $rootDir . '/parsedown-extra-0.7.1/ParsedownExtra.php';
|
||||
|
||||
if (is_file($parsedownPath)) {
|
||||
require_once $parsedownPath;
|
||||
}
|
||||
if (is_file($extraPath)) {
|
||||
require_once $extraPath;
|
||||
}
|
||||
|
||||
$html = '';
|
||||
if (class_exists('ParsedownExtra')) {
|
||||
$pd = new ParsedownExtra();
|
||||
$html = $pd->text($md);
|
||||
}
|
||||
|
||||
json_out([
|
||||
'slug' => $slug,
|
||||
'markdown' => $md,
|
||||
'html' => $html,
|
||||
]);
|
||||
51
terminal/api/pages.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$articlesDir = $rootDir . '/articles';
|
||||
if (!is_dir($articlesDir)) {
|
||||
json_out(['pages' => []]);
|
||||
}
|
||||
|
||||
$pages = [];
|
||||
$files = glob($articlesDir . '/*.md') ?: [];
|
||||
sort($files);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$slug = basename($file, '.md');
|
||||
|
||||
if (preg_match('/^success\d+$/', $slug) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$title = $slug;
|
||||
|
||||
$fh = fopen($file, 'rb');
|
||||
if ($fh !== false) {
|
||||
for ($i = 0; $i < 20; $i++) {
|
||||
$line = fgets($fh);
|
||||
if ($line === false) {
|
||||
break;
|
||||
}
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^#\s+(.+)$/', $line, $m)) {
|
||||
$title = trim($m[1]);
|
||||
break;
|
||||
}
|
||||
$title = mb_substr($line, 0, 80);
|
||||
break;
|
||||
}
|
||||
fclose($fh);
|
||||
}
|
||||
|
||||
$pages[] = [
|
||||
'slug' => $slug,
|
||||
'title' => $title,
|
||||
];
|
||||
}
|
||||
|
||||
json_out(['pages' => $pages]);
|
||||
49
terminal/api/server.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$candidates = [
|
||||
$rootDir . '/report',
|
||||
$rootDir . '/includes/report',
|
||||
];
|
||||
|
||||
$report = null;
|
||||
foreach ($candidates as $cand) {
|
||||
if (is_file($cand)) {
|
||||
$report = $cand;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($report === null) {
|
||||
json_out(['rows' => [], 'lastUpdated' => null]);
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$fh = fopen($report, 'rb');
|
||||
if ($fh === false) {
|
||||
json_out(['rows' => [], 'lastUpdated' => null]);
|
||||
}
|
||||
|
||||
while (($line = fgets($fh)) !== false) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$parts = str_getcsv($line);
|
||||
if (count($parts) < 3) {
|
||||
continue;
|
||||
}
|
||||
$rows[] = [
|
||||
'host' => (string)$parts[0],
|
||||
'check' => (string)$parts[1],
|
||||
'status' => (string)$parts[2],
|
||||
];
|
||||
}
|
||||
fclose($fh);
|
||||
|
||||
$mtime = @filemtime($report);
|
||||
$last = $mtime ? gmdate('c', (int)$mtime) : null;
|
||||
|
||||
json_out(['rows' => $rows, 'lastUpdated' => $last]);
|
||||
42
terminal/api/users.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$siteRoot = '//' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
|
||||
|
||||
$skelIndex = '/etc/skel/public_html/index.html';
|
||||
$skelIndexCksum = is_file($skelIndex) ? @md5_file($skelIndex) : null;
|
||||
|
||||
$users = [];
|
||||
$homes = glob('/home/*', GLOB_ONLYDIR) ?: [];
|
||||
|
||||
foreach ($homes as $homeDir) {
|
||||
$user = basename($homeDir);
|
||||
if ($user === '' || $user === 'lost+found') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$userIndex = $homeDir . '/public_html/index.html';
|
||||
$userPub = $homeDir . '/public_html';
|
||||
|
||||
if (!is_dir($userPub)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasCustomIndex = false;
|
||||
if (is_file($userIndex) && $skelIndexCksum !== null) {
|
||||
$userCksum = @md5_file($userIndex);
|
||||
$hasCustomIndex = ($userCksum !== false && $userCksum !== $skelIndexCksum);
|
||||
} elseif (is_file($userIndex)) {
|
||||
$hasCustomIndex = true;
|
||||
}
|
||||
|
||||
$users[] = [
|
||||
'username' => $user,
|
||||
'url' => $siteRoot . '/~' . rawurlencode($user) . '/',
|
||||
'hasContent' => $hasCustomIndex,
|
||||
];
|
||||
}
|
||||
|
||||
json_out(['users' => $users]);
|
||||
159
terminal/index.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
|
||||
|
||||
?><!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>thunix terminal</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
|
||||
<noscript><meta http-equiv="refresh" content="0;url=/main"></noscript>
|
||||
<!-- xterm core styles -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/css/xterm.css">
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--mono: "Departure Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bg: #111;
|
||||
--amber: #edb200;
|
||||
--amber-txt: #ffc828;
|
||||
--panel: rgba(255, 255, 255, 0.03);
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
*{ box-sizing:border-box; }
|
||||
|
||||
html, body{
|
||||
margin:0;
|
||||
padding:0;
|
||||
height:100%;
|
||||
width:100%;
|
||||
overflow:hidden;
|
||||
background: var(--bg);
|
||||
color: var(--amber);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
body::before{
|
||||
content:'';
|
||||
position:fixed;
|
||||
inset:0;
|
||||
pointer-events:none;
|
||||
z-index:2;
|
||||
background: linear-gradient(to bottom, transparent 50%, rgba(0,0,0,0.32) 51%);
|
||||
background-size: 100% 4px;
|
||||
}
|
||||
|
||||
#wrap{
|
||||
position:relative;
|
||||
z-index:1;
|
||||
height:100%;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
padding: 6vh 0 6vh 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#head{
|
||||
width: min(92vw, 1400px);
|
||||
margin: 0 auto;
|
||||
font-size: clamp(18px, 3vw, 44px);
|
||||
text-shadow: 0 0 1.75rem rgba(237,178,0,0.55);
|
||||
line-height:1.1;
|
||||
}
|
||||
#head small{
|
||||
display:block;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.85;
|
||||
margin-top: 8px;
|
||||
text-shadow:none;
|
||||
}
|
||||
|
||||
#main{
|
||||
width: min(92vw, 1400px);
|
||||
margin: 0 auto;
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1.05fr 1.25fr;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#terminalHost,
|
||||
#contentHost{
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 24px rgba(237,178,0,0.09);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
#terminal{ height: 100%; width: 100%; }
|
||||
|
||||
#contentFrame{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
@media (max-width: 1050px){
|
||||
#main{ grid-template-columns: 1fr; grid-template-rows: 48vh 1fr; }
|
||||
}
|
||||
|
||||
#terminalHost .xterm-rows a,
|
||||
#terminalHost .xterm-screen a{
|
||||
color: var(--amber-txt) !important;
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 3px;
|
||||
text-shadow: 0 0 0.85rem rgba(237,178,0,0.30);
|
||||
}
|
||||
#terminalHost .xterm-rows a:hover,
|
||||
#terminalHost .xterm-screen a:hover{
|
||||
filter: brightness(1.08);
|
||||
text-shadow: 0 0 1.25rem rgba(237,178,0,0.55);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<noscript>
|
||||
<div style="max-width: 900px; margin: 2rem auto; padding: 1.25rem 1.5rem; border: 1px solid rgba(255,255,255,0.12); border-radius: 14px; background: rgba(0,0,0,0.55); color: #e6e6e6; font-family: system-ui, -apple-system, Segoe UI, sans-serif;">
|
||||
<h1 style="margin: 0 0 0.6rem 0; font-size: 1.35rem; font-weight: 700;">Terminal mode needs JavaScript</h1>
|
||||
<p style="margin: 0; opacity: 0.9; line-height: 1.4;">JavaScript is disabled, so the terminal UI can’t run. Redirecting you to the classic site… If you’re not redirected, use <a href="/main" style="color: #6ec5ff; text-decoration: underline;">the classic site</a>.</p>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="wrap">
|
||||
<div id="head">
|
||||
🌻 thunix
|
||||
<small>Type <strong>help</strong>. Click inside the terminal to focus.</small>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div id="terminalHost">
|
||||
<div id="terminal" aria-label="Terminal"></div>
|
||||
</div>
|
||||
|
||||
<div id="contentHost" aria-label="Content">
|
||||
<iframe
|
||||
id="contentFrame"
|
||||
src="/terminal/view.php?page=main"
|
||||
title="thunix content"
|
||||
referrerpolicy="no-referrer"
|
||||
loading="eager"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/terminal/terminal.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
545
terminal/terminal.js
Normal file
@@ -0,0 +1,545 @@
|
||||
// thunix terminal UI powered by xterm.js (ESM via jsDelivr).
|
||||
|
||||
import { Terminal } from "https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/+esm";
|
||||
import { FitAddon } from "https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.11.0/+esm";
|
||||
import { WebLinksAddon } from "https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.12.0/+esm";
|
||||
|
||||
const MODULE_BASE = new URL(".", import.meta.url);
|
||||
const urlFromBase = (path) => new URL(path, MODULE_BASE).toString();
|
||||
|
||||
const DEFAULT_WEBMAIL_PATH = "/webmail/";
|
||||
|
||||
const ESC = "\x1b";
|
||||
const CSI = `${ESC}[`;
|
||||
const OSC = `${ESC}]`;
|
||||
|
||||
const ANSI = {
|
||||
reset: `${CSI}0m`,
|
||||
bold: `${CSI}1m`,
|
||||
dim: `${CSI}2m`,
|
||||
underline: `${CSI}4m`,
|
||||
noUnderline: `${CSI}24m`,
|
||||
clrLine: `${CSI}2K`,
|
||||
fgBrightYellow: `${CSI}93m`,
|
||||
fgWhite: `${CSI}37m`,
|
||||
};
|
||||
|
||||
function osc8(url, text) {
|
||||
const BEL = "\x07";
|
||||
return `${OSC}8;;${url}${BEL}${text}${OSC}8;;${BEL}`;
|
||||
}
|
||||
|
||||
function absolutizeUrl(href) {
|
||||
if (!href) return href;
|
||||
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(href)) return href;
|
||||
if (href.startsWith("//")) return `${location.protocol}${href}`;
|
||||
if (href.startsWith("/")) return `${location.origin}${href}`;
|
||||
return new URL(href, location.href).toString();
|
||||
}
|
||||
|
||||
function renderInlineLinks(line) {
|
||||
return line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text, href) => {
|
||||
const url = absolutizeUrl(String(href).trim());
|
||||
const label = `${ANSI.underline}${ANSI.fgBrightYellow}${text}${ANSI.reset}`;
|
||||
return osc8(url, label);
|
||||
});
|
||||
}
|
||||
|
||||
function stripHtmlToText(html) {
|
||||
let s = String(html || "");
|
||||
|
||||
s = s.replace(/<\s*br\s*\/?\s*>/gi, "\n");
|
||||
s = s.replace(/<\s*\/(p|div|tr|li|table|form|h1|h2|h3|ul|ol)\s*>/gi, "\n");
|
||||
s = s.replace(/<\s*(p|div|tr|li|table|form|h1|h2|h3|ul|ol)(\s[^>]*)?>/gi, "\n");
|
||||
|
||||
s = s.replace(/<a\s+[^>]*href=['"]([^'"]+)['"][^>]*>(.*?)<\/a>/gi, (_m, href, text) => {
|
||||
const cleanText = String(text).replace(/<[^>]+>/g, "").trim() || href;
|
||||
return `[${cleanText}](${href})`;
|
||||
});
|
||||
|
||||
s = s.replace(/<[^>]+>/g, "");
|
||||
|
||||
s = s.replace(/ /g, " ");
|
||||
s = s.replace(/&/g, "&");
|
||||
s = s.replace(/</g, "<");
|
||||
s = s.replace(/>/g, ">");
|
||||
s = s.replace(/"/g, '"');
|
||||
s = s.replace(/'/g, "'");
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function renderMarkdown(md) {
|
||||
const input = stripHtmlToText(md);
|
||||
const out = [];
|
||||
const lines = input.replace(/\r\n/g, "\n").split("\n");
|
||||
|
||||
let inCode = false;
|
||||
|
||||
for (const raw of lines) {
|
||||
let line = raw;
|
||||
|
||||
if (/^\s*```/.test(line)) {
|
||||
inCode = !inCode;
|
||||
out.push(inCode ? `${ANSI.dim}--- code ---${ANSI.reset}` : `${ANSI.dim}--- end ---${ANSI.reset}`);
|
||||
continue;
|
||||
}
|
||||
if (inCode) {
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^\s*#\s+/.test(line)) {
|
||||
const title = line.replace(/^\s*#\s+/, "").trim();
|
||||
out.push(`${ANSI.bold}${title}${ANSI.reset}`);
|
||||
out.push(`${ANSI.dim}${"=".repeat(Math.min(78, title.length || 1))}${ANSI.reset}`);
|
||||
continue;
|
||||
}
|
||||
if (/^\s*##\s+/.test(line)) {
|
||||
const title = line.replace(/^\s*##\s+/, "").trim();
|
||||
out.push(`${ANSI.bold}${title}${ANSI.reset}`);
|
||||
out.push(`${ANSI.dim}${"-".repeat(Math.min(78, title.length || 1))}${ANSI.reset}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const mBullet = line.match(/^\s*[-*]\s+(.*)$/);
|
||||
if (mBullet) {
|
||||
line = ` - ${mBullet[1]}`;
|
||||
}
|
||||
|
||||
line = line.replace(/\s+$/g, "");
|
||||
line = renderInlineLinks(line);
|
||||
out.push(line);
|
||||
}
|
||||
|
||||
return out.join("\r\n");
|
||||
}
|
||||
|
||||
class ThunixTerminal {
|
||||
constructor(hostEl, frameEl) {
|
||||
this.el = hostEl;
|
||||
this.frame = frameEl;
|
||||
|
||||
this.term = null;
|
||||
this.fit = new FitAddon();
|
||||
|
||||
this.buffer = "";
|
||||
this.history = [];
|
||||
this.historyIdx = -1;
|
||||
|
||||
this.pages = new Map();
|
||||
this.menu = null;
|
||||
|
||||
this.webLinks = new WebLinksAddon((ev, uri) => {
|
||||
try {
|
||||
ev?.preventDefault?.();
|
||||
} catch {
|
||||
}
|
||||
this.handleLinkActivate(uri);
|
||||
});
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.term = new Terminal({
|
||||
cursorBlink: true,
|
||||
convertEol: true,
|
||||
scrollback: 4000,
|
||||
fontFamily:
|
||||
'"Departure Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
fontSize: 18,
|
||||
theme: {
|
||||
background: "#111111",
|
||||
foreground: "#edb200",
|
||||
cursor: "#ffc828",
|
||||
selection: "rgba(255, 200, 40, 0.25)",
|
||||
black: "#000000",
|
||||
brightYellow: "#ffc828",
|
||||
},
|
||||
});
|
||||
|
||||
this.term.loadAddon(this.fit);
|
||||
this.term.loadAddon(this.webLinks);
|
||||
|
||||
this.term.open(this.el);
|
||||
this.fit.fit();
|
||||
|
||||
window.addEventListener("resize", () => this.fit.fit());
|
||||
|
||||
this.el.addEventListener("mousedown", () => this.term.focus());
|
||||
this.term.focus();
|
||||
|
||||
this.term.onKey((e) => this.onKey(e));
|
||||
|
||||
this.boot();
|
||||
}
|
||||
|
||||
writeln(s = "") {
|
||||
this.term.writeln(s);
|
||||
}
|
||||
|
||||
write(s = "") {
|
||||
this.term.write(s);
|
||||
}
|
||||
|
||||
prompt() {
|
||||
this.write(`\r\n${ANSI.bold}guest@thunix${ANSI.reset}:${ANSI.fgWhite}~${ANSI.reset}$ `);
|
||||
}
|
||||
|
||||
redrawInput(newValue) {
|
||||
this.write(`\r${ANSI.clrLine}${ANSI.bold}guest@thunix${ANSI.reset}:${ANSI.fgWhite}~${ANSI.reset}$ ${newValue}`);
|
||||
}
|
||||
|
||||
async boot() {
|
||||
this.writeln(`${ANSI.bold}thunix terminal${ANSI.reset}`);
|
||||
this.writeln(`${ANSI.dim}Terminal commands + real HTML panel so forms actually work.${ANSI.reset}`);
|
||||
this.writeln("");
|
||||
|
||||
await Promise.allSettled([this.loadPages(), this.loadMenu()]);
|
||||
|
||||
const hash = (location.hash || "").replace(/^#/, "").trim();
|
||||
const initial = hash && this.normalizeSlug(hash);
|
||||
if (initial) {
|
||||
this.openInPanel(initial);
|
||||
} else {
|
||||
this.openInPanel("main");
|
||||
}
|
||||
|
||||
this.writeln(`${ANSI.dim}Type ${ANSI.reset}${ANSI.bold}help${ANSI.reset}${ANSI.dim} for commands.${ANSI.reset}`);
|
||||
this.prompt();
|
||||
}
|
||||
|
||||
async loadPages() {
|
||||
const res = await fetch(urlFromBase("api/pages.php"), { cache: "no-store" });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (!data?.pages) return;
|
||||
for (const p of data.pages) {
|
||||
this.pages.set(p.slug, p.title || p.slug);
|
||||
}
|
||||
}
|
||||
|
||||
async loadMenu() {
|
||||
const res = await fetch(urlFromBase("api/menu.php"), { cache: "no-store" });
|
||||
if (!res.ok) return;
|
||||
this.menu = await res.json();
|
||||
}
|
||||
|
||||
handleLinkActivate(uri) {
|
||||
const u = (() => {
|
||||
try {
|
||||
return new URL(uri, location.origin);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!u) {
|
||||
window.open(uri, "_blank", "noopener");
|
||||
return;
|
||||
}
|
||||
|
||||
if (u.pathname.endsWith("/terminal/view.php")) {
|
||||
const p = u.searchParams.get("page") || "";
|
||||
if (p) {
|
||||
this.openInPanel(p);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (u.origin === location.origin && u.pathname.startsWith("/")) {
|
||||
const slug = this.normalizeSlug(u.pathname.replace(/^\/+/, ""));
|
||||
if (slug && this.pages.has(slug)) {
|
||||
this.openInPanel(slug);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
window.open(u.toString(), "_blank", "noopener");
|
||||
}
|
||||
|
||||
onKey({ key, domEvent }) {
|
||||
const ev = domEvent;
|
||||
|
||||
if (ev.ctrlKey && ev.key.toLowerCase() === "l") {
|
||||
ev.preventDefault();
|
||||
this.cmdClear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.ctrlKey && ev.key.toLowerCase() === "c") {
|
||||
ev.preventDefault();
|
||||
this.write("^C");
|
||||
this.buffer = "";
|
||||
this.historyIdx = -1;
|
||||
this.prompt();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "Enter") {
|
||||
const line = this.buffer.trim();
|
||||
this.buffer = "";
|
||||
this.historyIdx = -1;
|
||||
this.write("\r\n");
|
||||
if (line) this.history.unshift(line);
|
||||
this.run(line);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "Backspace") {
|
||||
if (this.buffer.length > 0) {
|
||||
this.buffer = this.buffer.slice(0, -1);
|
||||
this.write("\b \b");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "ArrowUp") {
|
||||
if (this.history.length === 0) return;
|
||||
if (this.historyIdx + 1 < this.history.length) this.historyIdx++;
|
||||
const next = this.history[this.historyIdx] ?? "";
|
||||
this.buffer = next;
|
||||
this.redrawInput(this.buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "ArrowDown") {
|
||||
if (this.history.length === 0) return;
|
||||
if (this.historyIdx > 0) this.historyIdx--;
|
||||
else this.historyIdx = -1;
|
||||
const next = this.historyIdx >= 0 ? (this.history[this.historyIdx] ?? "") : "";
|
||||
this.buffer = next;
|
||||
this.redrawInput(this.buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ev.altKey && !ev.ctrlKey && !ev.metaKey && key && key.length === 1) {
|
||||
this.buffer += key;
|
||||
this.write(key);
|
||||
}
|
||||
}
|
||||
|
||||
async run(line) {
|
||||
if (!line) {
|
||||
this.prompt();
|
||||
return;
|
||||
}
|
||||
|
||||
const [cmdRaw, ...args] = line.split(/\s+/);
|
||||
const cmd = cmdRaw.toLowerCase();
|
||||
|
||||
switch (cmd) {
|
||||
case "help":
|
||||
this.cmdHelp();
|
||||
break;
|
||||
case "clear":
|
||||
this.cmdClear();
|
||||
break;
|
||||
case "menu":
|
||||
await this.cmdMenu();
|
||||
break;
|
||||
case "pages":
|
||||
case "ls":
|
||||
await this.cmdPages();
|
||||
break;
|
||||
case "open":
|
||||
await this.cmdOpen(args.join(" "));
|
||||
break;
|
||||
case "cat":
|
||||
await this.cmdCat(args.join(" "));
|
||||
break;
|
||||
case "web":
|
||||
this.cmdWeb(args.join(" "));
|
||||
break;
|
||||
case "webmail":
|
||||
case "mail":
|
||||
this.cmdWebmail(args.join(" "));
|
||||
break;
|
||||
case "users":
|
||||
await this.cmdOpen("users");
|
||||
break;
|
||||
case "server":
|
||||
await this.cmdOpen("server");
|
||||
break;
|
||||
case "news":
|
||||
await this.cmdOpen("news");
|
||||
break;
|
||||
case "main":
|
||||
case "home":
|
||||
await this.cmdOpen("main");
|
||||
break;
|
||||
default:
|
||||
this.writeln(`${ANSI.dim}Unknown command:${ANSI.reset} ${cmdRaw}`);
|
||||
this.writeln(`${ANSI.dim}Try:${ANSI.reset} ${ANSI.bold}help${ANSI.reset}`);
|
||||
break;
|
||||
}
|
||||
|
||||
this.prompt();
|
||||
}
|
||||
|
||||
cmdHelp() {
|
||||
this.writeln(`${ANSI.bold}Commands${ANSI.reset}`);
|
||||
this.writeln(`${ANSI.dim}help${ANSI.reset} Show this help`);
|
||||
this.writeln(`${ANSI.dim}pages | ls${ANSI.reset} List available content pages`);
|
||||
this.writeln(`${ANSI.dim}open <page>${ANSI.reset} Load a page in the panel`);
|
||||
this.writeln(`${ANSI.dim}web <page>${ANSI.reset} Print web URLs for a page`);
|
||||
this.writeln(`${ANSI.dim}webmail [url]${ANSI.reset} Open webmail in a new tab (alias: mail)`);
|
||||
this.writeln(`${ANSI.dim}clear${ANSI.reset} Clear the terminal (Ctrl+L)`);
|
||||
this.writeln("");
|
||||
this.writeln(`${ANSI.dim}Aliases:${ANSI.reset} home, main, users, server, news, mail`);
|
||||
}
|
||||
|
||||
cmdClear() {
|
||||
this.term.clear();
|
||||
this.term.reset();
|
||||
this.writeln(`${ANSI.bold}thunix terminal${ANSI.reset}`);
|
||||
}
|
||||
|
||||
async cmdMenu() {
|
||||
if (!this.menu) await this.loadMenu();
|
||||
const menu = this.menu;
|
||||
|
||||
if (!menu?.sections?.length) {
|
||||
this.writeln(`${ANSI.dim}Menu not available.${ANSI.reset}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const section of menu.sections) {
|
||||
this.writeln("");
|
||||
this.writeln(`${ANSI.bold}${section.title}${ANSI.reset}`);
|
||||
this.writeln(`${ANSI.dim}${"-".repeat(Math.min(78, section.title.length || 1))}${ANSI.reset}`);
|
||||
for (const item of section.items) {
|
||||
const href = item.internal ? `${location.origin}/${item.slug}` : absolutizeUrl(item.href);
|
||||
const label = `${ANSI.underline}${ANSI.fgBrightYellow}${item.text}${ANSI.reset}`;
|
||||
const link = osc8(href, label);
|
||||
const hint = item.internal ? `${ANSI.dim} (open ${item.slug})${ANSI.reset}` : "";
|
||||
this.writeln(` • ${link}${hint}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async cmdPages() {
|
||||
if (this.pages.size === 0) await this.loadPages();
|
||||
if (this.pages.size === 0) {
|
||||
this.writeln(`${ANSI.dim}No pages found.${ANSI.reset}`);
|
||||
return;
|
||||
}
|
||||
this.writeln(`${ANSI.bold}Pages${ANSI.reset}`);
|
||||
const slugs = [...this.pages.keys()].sort();
|
||||
this.writeln(slugs.map((s) => ` - ${s}`).join("\r\n"));
|
||||
}
|
||||
|
||||
cmdWeb(arg) {
|
||||
const slug = this.normalizeSlug(arg);
|
||||
if (!slug) {
|
||||
this.writeln(`${ANSI.dim}Usage:${ANSI.reset} web <page>`);
|
||||
return;
|
||||
}
|
||||
const classic = `${location.origin}/${slug}`;
|
||||
const panelUrl = new URL("view.php", MODULE_BASE);
|
||||
panelUrl.searchParams.set("page", slug);
|
||||
const panel = panelUrl.toString();
|
||||
this.writeln(`${ANSI.dim}Classic:${ANSI.reset} ${osc8(classic, `${ANSI.underline}${ANSI.fgBrightYellow}${classic}${ANSI.reset}`)}`);
|
||||
this.writeln(`${ANSI.dim}Panel:${ANSI.reset} ${osc8(panel, `${ANSI.underline}${ANSI.fgBrightYellow}${panel}${ANSI.reset}`)}`);
|
||||
}
|
||||
|
||||
cmdWebmail(arg) {
|
||||
const raw = String(arg || "").trim();
|
||||
|
||||
const configured = (() => {
|
||||
try {
|
||||
const v = window.THUNIX_WEBMAIL_URL;
|
||||
return typeof v === "string" ? v.trim() : "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
})();
|
||||
|
||||
const target = raw || configured || DEFAULT_WEBMAIL_PATH;
|
||||
const url = absolutizeUrl(target);
|
||||
|
||||
window.open(url, "_blank", "noopener");
|
||||
|
||||
const label = `${ANSI.underline}${ANSI.fgBrightYellow}${url}${ANSI.reset}`;
|
||||
this.writeln(`${ANSI.dim}Opened webmail:${ANSI.reset} ${osc8(url, label)}`);
|
||||
|
||||
if (!raw && !configured && DEFAULT_WEBMAIL_PATH !== "/webmail/") {
|
||||
this.writeln(`${ANSI.dim}Hint:${ANSI.reset} set window.THUNIX_WEBMAIL_URL if your webmail lives elsewhere.`);
|
||||
} else if (!raw && !configured) {
|
||||
this.writeln(
|
||||
`${ANSI.dim}Hint:${ANSI.reset} if your webmail isn't at ${ANSI.bold}${DEFAULT_WEBMAIL_PATH}${ANSI.reset}${ANSI.dim}, set window.THUNIX_WEBMAIL_URL (or run: webmail <url>).${ANSI.reset}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
normalizeSlug(arg) {
|
||||
if (!arg) return "";
|
||||
let s = String(arg).trim();
|
||||
if (s.startsWith("/")) s = s.replace(/^\/+/, "");
|
||||
if (s.includes("?")) s = s.split("?")[0];
|
||||
if (s === "") return "";
|
||||
return s;
|
||||
}
|
||||
|
||||
openInPanel(slug) {
|
||||
if (!this.frame) return;
|
||||
const clean = this.normalizeSlug(slug);
|
||||
this.frame.src = `${urlFromBase("view.php")}?page=${encodeURIComponent(clean)}`;
|
||||
|
||||
try {
|
||||
history.replaceState(null, "", `#${encodeURIComponent(clean)}`);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
async cmdOpen(arg) {
|
||||
const slug = this.normalizeSlug(arg);
|
||||
if (!slug) {
|
||||
this.writeln(`${ANSI.dim}Usage:${ANSI.reset} open <page>`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(slug) || slug.startsWith("//")) {
|
||||
const url = absolutizeUrl(slug);
|
||||
window.open(url, "_blank", "noopener");
|
||||
this.writeln(`${ANSI.dim}Opened externally:${ANSI.reset} ${osc8(url, `${ANSI.underline}${ANSI.fgBrightYellow}${url}${ANSI.reset}`)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.pages.size === 0) await this.loadPages();
|
||||
if (this.pages.size && !this.pages.has(slug) && !/^success\d+$/.test(slug)) {
|
||||
this.writeln(`${ANSI.dim}No such page:${ANSI.reset} ${slug}`);
|
||||
this.writeln(`${ANSI.dim}Try:${ANSI.reset} pages`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.openInPanel(slug);
|
||||
this.writeln(`${ANSI.dim}Loaded in panel:${ANSI.reset} ${ANSI.bold}${slug}${ANSI.reset}`);
|
||||
}
|
||||
|
||||
async cmdCat(arg) {
|
||||
const slug = this.normalizeSlug(arg);
|
||||
if (!slug) {
|
||||
this.writeln(`${ANSI.dim}Usage:${ANSI.reset} cat <page>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`${urlFromBase("api/page.php")}?p=${encodeURIComponent(slug)}`, { cache: "no-store" });
|
||||
if (!res.ok) {
|
||||
this.writeln(`${ANSI.dim}No such page:${ANSI.reset} ${slug}`);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
const md = String(data?.markdown ?? "");
|
||||
this.writeln("");
|
||||
this.writeln(renderMarkdown(md));
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const host = document.getElementById("terminal");
|
||||
const frame = document.getElementById("contentFrame");
|
||||
if (!host) return;
|
||||
new ThunixTerminal(host, frame);
|
||||
});
|
||||
234
terminal/view.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// Render wiki content using the *same* parser stack as wiki.php,
|
||||
|
||||
require __DIR__ . '/../config.php';
|
||||
require __DIR__ . '/../parsedown-1.7.3/Parsedown.php';
|
||||
require __DIR__ . '/../parsedown-extra-0.7.1/ParsedownExtra.php';
|
||||
|
||||
$page = isset($_GET['page']) ? (string) $_GET['page'] : 'main';
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/', $page)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
echo "Bad page name.";
|
||||
exit;
|
||||
}
|
||||
|
||||
$contentPath = $doc_root . '/articles/' . $page . '.md';
|
||||
if (!is_file($contentPath)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
echo "Not found.";
|
||||
exit;
|
||||
}
|
||||
|
||||
$ParsedownExtra = new ParsedownExtra();
|
||||
|
||||
$md = file_get_contents($contentPath);
|
||||
if ($md === false) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
echo "Failed to read page.";
|
||||
exit;
|
||||
}
|
||||
|
||||
$html = $ParsedownExtra->text($md);
|
||||
|
||||
if ($page === 'users' || $page === 'server') {
|
||||
$inc = $doc_root . '/includes/' . $page . '.php';
|
||||
if (is_file($inc)) {
|
||||
ob_start();
|
||||
require $inc;
|
||||
$html .= (string) ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
||||
$knownPages = [];
|
||||
foreach (glob($doc_root . '/articles/*.md') ?: [] as $file) {
|
||||
$slug = basename($file, '.md');
|
||||
$knownPages[$slug] = true;
|
||||
}
|
||||
|
||||
$rewriteFormActions = function (string $action): string {
|
||||
$path = parse_url($action, PHP_URL_PATH);
|
||||
if (!is_string($path) || $path === '') {
|
||||
$path = $action;
|
||||
}
|
||||
|
||||
$pathLower = strtolower($path);
|
||||
if (!preg_match('~(^|/)(includes/(contact|signup)\.php)$~', $pathLower)) {
|
||||
return $action;
|
||||
}
|
||||
|
||||
$parts = parse_url($action);
|
||||
if ($parts === false) {
|
||||
if (strpos($action, 'terminal=1') !== false) {
|
||||
return $action;
|
||||
}
|
||||
return (strpos($action, '?') !== false) ? ($action . '&terminal=1') : ($action . '?terminal=1');
|
||||
}
|
||||
|
||||
$query = [];
|
||||
if (isset($parts['query'])) {
|
||||
parse_str((string) $parts['query'], $query);
|
||||
}
|
||||
$query['terminal'] = '1';
|
||||
$queryString = http_build_query($query);
|
||||
|
||||
$rebuilt = '';
|
||||
if (isset($parts['scheme'])) {
|
||||
$rebuilt .= $parts['scheme'] . '://';
|
||||
} elseif (str_starts_with($action, '//')) {
|
||||
$rebuilt .= '//';
|
||||
}
|
||||
|
||||
if (isset($parts['user'])) {
|
||||
$rebuilt .= $parts['user'];
|
||||
if (isset($parts['pass'])) {
|
||||
$rebuilt .= ':' . $parts['pass'];
|
||||
}
|
||||
$rebuilt .= '@';
|
||||
}
|
||||
|
||||
if (isset($parts['host'])) {
|
||||
$rebuilt .= $parts['host'];
|
||||
}
|
||||
if (isset($parts['port'])) {
|
||||
$rebuilt .= ':' . $parts['port'];
|
||||
}
|
||||
|
||||
$rebuilt .= $parts['path'] ?? '';
|
||||
if ($queryString !== '') {
|
||||
$rebuilt .= '?' . $queryString;
|
||||
}
|
||||
if (isset($parts['fragment'])) {
|
||||
$rebuilt .= '#' . $parts['fragment'];
|
||||
}
|
||||
|
||||
return $rebuilt;
|
||||
};
|
||||
|
||||
|
||||
$isTerminalSuccessFormTarget = function (string $action): bool {
|
||||
$path = parse_url($action, PHP_URL_PATH);
|
||||
if (!is_string($path) || $path === '') {
|
||||
$path = $action;
|
||||
}
|
||||
|
||||
$pathLower = strtolower($path);
|
||||
return preg_match('~(^|/)(includes/(contact|signup)\.php)$~', $pathLower) === 1;
|
||||
};
|
||||
|
||||
|
||||
$html = preg_replace_callback(
|
||||
'~(<form\b[^>]*\baction\s*=\s*)(["\'])([^"\']+)(\2)~i',
|
||||
function (array $m) use ($rewriteFormActions): string {
|
||||
$new = $rewriteFormActions((string) $m[3]);
|
||||
return $m[1] . $m[2] . $new . $m[4];
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
||||
$html = preg_replace_callback(
|
||||
'~(<form\b[^>]*>)~i',
|
||||
function (array $m) use ($rewriteFormActions, $isTerminalSuccessFormTarget): string {
|
||||
$tag = $m[1];
|
||||
|
||||
if (preg_match('~\baction\s*=\s*(["\'])([^"\']+)\1~i', $tag, $am) !== 1) {
|
||||
return $tag;
|
||||
}
|
||||
|
||||
$action = (string) $am[2];
|
||||
if ($isTerminalSuccessFormTarget($action) === false) {
|
||||
return $tag;
|
||||
}
|
||||
|
||||
$newAction = $rewriteFormActions($action);
|
||||
$tag = preg_replace(
|
||||
'~\baction\s*=\s*(["\'])([^"\']+)\1~i',
|
||||
'action=' . $am[1] . $newAction . $am[1],
|
||||
$tag,
|
||||
1
|
||||
);
|
||||
|
||||
return $tag . "\n" . '<input type="hidden" name="terminal" value="1">';
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$dom = new DOMDocument();
|
||||
$dom->loadHTML(
|
||||
'<!doctype html><html><head><meta charset="utf-8"></head><body><div id="content">' . $html . '</div></body></html>',
|
||||
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
|
||||
);
|
||||
|
||||
foreach ($dom->getElementsByTagName('a') as $a) {
|
||||
$href = $a->getAttribute('href');
|
||||
if ($href === '' || str_starts_with($href, '#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^[a-zA-Z][a-zA-Z0-9+.-]*:/', $href) === 1) {
|
||||
$a->setAttribute('target', '_blank');
|
||||
$a->setAttribute('rel', 'noopener');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($href, '/')) {
|
||||
$path = parse_url($href, PHP_URL_PATH) ?? '';
|
||||
$slug = ltrim($path, '/');
|
||||
|
||||
if ($slug !== '' && isset($knownPages[$slug])) {
|
||||
$a->setAttribute('href', './view.php?page=' . rawurlencode($slug));
|
||||
continue;
|
||||
}
|
||||
|
||||
$a->setAttribute('target', '_blank');
|
||||
$a->setAttribute('rel', 'noopener');
|
||||
continue;
|
||||
}
|
||||
|
||||
$a->setAttribute('target', '_blank');
|
||||
$a->setAttribute('rel', 'noopener');
|
||||
}
|
||||
|
||||
foreach ($dom->getElementsByTagName('form') as $form) {
|
||||
$action = $form->getAttribute('action');
|
||||
if ($action !== '') {
|
||||
$form->setAttribute('action', $rewriteFormActions($action));
|
||||
}
|
||||
}
|
||||
|
||||
$finalHtml = $dom->saveHTML();
|
||||
libxml_clear_errors();
|
||||
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
|
||||
?><!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?php echo htmlspecialchars($site_name . ' - ' . $page, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
<link rel="stylesheet" type="text/css" href="<?php echo htmlspecialchars($site_root . '/includes/terminal.css', ENT_QUOTES, 'UTF-8'); ?>">
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
$dom2 = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$dom2->loadHTML($finalHtml);
|
||||
libxml_clear_errors();
|
||||
$content = $dom2->getElementById('content');
|
||||
if ($content === null) {
|
||||
echo $finalHtml;
|
||||
} else {
|
||||
echo $dom2->saveHTML($content);
|
||||
}
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
1460
tilde.json
34
wiki.php
@@ -8,24 +8,20 @@ include('config.php');
|
||||
include('parsedown-1.7.3/Parsedown.php');
|
||||
include('parsedown-extra-0.7.1/ParsedownExtra.php');
|
||||
|
||||
$page = $_GET['page'];
|
||||
$style = $_GET['style'];
|
||||
if(isset($_GET['page']))
|
||||
$page = $_GET['page'];
|
||||
else
|
||||
$page = "main";
|
||||
|
||||
if(isset($_GET['style']))
|
||||
$site_style = $_GET['style'];
|
||||
|
||||
$Parsedown = new Parsedown();
|
||||
$Parsedown->setMarkupEscaped(true);
|
||||
$ParsedownExtra = new ParsedownExtra();
|
||||
|
||||
if ( $page == "") {
|
||||
$page = "main";
|
||||
}
|
||||
|
||||
if ( $style == "") {
|
||||
if ( $site_style == "") {
|
||||
$site_style="site";
|
||||
}
|
||||
}
|
||||
else {
|
||||
$site_style=$style;
|
||||
}
|
||||
if (empty($site_style))
|
||||
$site_style="site";
|
||||
|
||||
$header = file_get_contents("$doc_root/includes/header.md");
|
||||
$sidebar = file_get_contents("$doc_root/includes/sidebar.md");
|
||||
@@ -67,6 +63,13 @@ print " </div>
|
||||
|
||||
echo $ParsedownExtra->text($content);
|
||||
|
||||
switch ($page)
|
||||
{
|
||||
case 'users':
|
||||
case 'server':
|
||||
require "includes/$page.php";
|
||||
}
|
||||
|
||||
print " </div>
|
||||
<!-- End Body -->
|
||||
|
||||
@@ -79,7 +82,8 @@ print " </div>
|
||||
|
||||
echo $Parsedown->text($footer);
|
||||
|
||||
print " </div>
|
||||
print " <a href=\"https://github.com/ThunixdotNet/www\">Site Source</a> | <a href=\"https://github.com/ThunixdotNet/www/tree/master/articles/$page.md\">Page Source</a>
|
||||
</div>
|
||||
<!-- End Footer -->
|
||||
|
||||
</body>
|
||||
|
||||