Created
December 8, 2023 06:28
-
-
Save jart/f79f98dd06a6f2413a775c5d39948176 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Copyright 2023 Justine Alexandra Roberts Tunney | |
# | |
# Permission to use, copy, modify, and/or distribute this software for | |
# any purpose with or without fee is hereby granted, provided that the | |
# above copyright notice and this permission notice appear in all copies. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL | |
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED | |
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE | |
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL | |
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR | |
# PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER | |
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | |
# PERFORMANCE OF THIS SOFTWARE. | |
import cosmo | |
import errno | |
import http.server | |
import math | |
import os | |
import resource | |
import signal | |
import socketserver | |
import sys | |
import time | |
import tokenbucket | |
import traceback | |
import urllib.parse | |
# public python sandbox | |
# secured by pledge() and unveil() | |
# | |
# git clone https://github.com/jart/cosmopolitan | |
# cd cosmopolitan | |
# make -j8 o//third_party/python/python.com | |
# o//third_party/python/python.com pledgethon.py 8080 | |
# google-chrome http://127.0.0.1:8080 | |
REPLENISH = 30. # seconds to grant a token | |
BUCKETBIT = 24 # netmask bits for each bucket | |
BURSTNESS = 5 # number of fast executions allowed | |
TIM_LIMIT = 3. # max seconds of wall time for client | |
THR_LIMIT = (0, 0) # max numbers of threads on whole system | |
CPU_LIMIT = (1, 2) # max seconds of cpu time for each client | |
FDS_LIMIT = (128, 128) # highest numbered file descriptor allowed | |
FSZ_LIMIT = (500*1000, 512*1000) # maximum number of bytes allowed in file | |
RAM_LIMIT = (64*1024*1024, 65*1024*1024) # bytes of ram client is allowed to have | |
SERVER_PLEDGE = 'stdio rpath wpath cpath anet proc unveil' # system calls allowed for whole server | |
CLIENT_PLEDGE = 'stdio rpath wpath cpath' # subset of above syscalls for clients | |
CODE_LOG = '/home/jart/code.log' | |
std_print = print # make print() non-buffered to stdout | |
print = lambda *args, **kwargs: std_print(*args, **kwargs, flush=True) | |
def run(cmd): | |
print(cmd) | |
os.system(cmd) | |
def on_sigalrm(signum, frame): | |
os.write(2, b'*** RAN OUT OF WALL TIME ***\n') | |
os.kill(pid, signal.SIGKILL) | |
def on_sigxcpu(signum, frame): | |
os.write(2, b'*** RAN OUT OF CPU QUOTA ***\n') | |
os._exit(128 + signal.SIGXCPU) | |
def on_sigxfsz(signum, frame): | |
os.write(2, b'*** RAN OUT OF FILE QUOTA ***\n') | |
os._exit(128 + signal.SIGXFSZ) | |
class Server(socketserver.ForkingMixIn, http.server.HTTPServer): | |
max_children = 256 | |
protocol_version = 'HTTP/1.0' | |
def forked_request(self, request, client_address): | |
resource.setrlimit(resource.RLIMIT_AS, RAM_LIMIT) | |
resource.setrlimit(resource.RLIMIT_CPU, CPU_LIMIT) | |
os.close(0) # close standard input | |
os.close(3) # close server socket | |
cosmo.verynice() # low client priority | |
cosmo.unveil('/public', 'rwc') # unveil the fun folder | |
cosmo.unveil('/proc/cpuinfo', 'r') # unveil this file to our visitors | |
cosmo.unveil(CODE_LOG, 'w') # unveil this file to our visitors | |
cosmo.unveil(None, None) # commit filesystem policy | |
os.chdir('/public') # go to fun folder | |
class Handler(http.server.SimpleHTTPRequestHandler): | |
protocol_version = 'HTTP/1.0' | |
server_version = 'pledgethon/1.o' | |
def do_GET(self): | |
self.close_connection = True | |
self.send_response(200) | |
content = b'''\ | |
<!doctype html> | |
<html lang="en"> | |
<meta charset="utf-8"> | |
<title>Redbean Systems</title> | |
<style> | |
body { | |
max-width: 960px; | |
min-width: 960px; | |
margin: 0 auto 0 auto; | |
margin-top: 2em; | |
background: white; | |
} | |
input, | |
textarea { | |
margin-top: .5em; | |
margin-bottom: .5em; | |
padding: .5em; | |
} | |
</style> | |
<h1>Redbean Systems</h1> | |
<form action="." method="post" target="output"> | |
<textarea id="code" name="code" cols="80" rows="20" autofocus> | |
import os | |
print(os.environ['USER']) | |
print(os.getcwd()) | |
with open('/proc/cpuinfo') as f: | |
print(f.read()) | |
</textarea><br> | |
<input type="submit" value="run my code"> | |
</form> | |
<iframe name="output" width="960" height="400" frameBorder="0"></iframe> | |
<h2>source code</h2> | |
<pre> | |
%s | |
</pre> | |
''' % (SAUCE) | |
self.send_header('Content-Type', 'text/html; charset=utf-8') | |
self.send_header('Cache-Control', 'max-age=0, private, no-store'); | |
self.send_header('Content-Length', str(len(content))) | |
self.send_header('Connection', 'close') | |
self.end_headers() | |
self.wfile.write(content) | |
self.wfile.flush() | |
def do_POST(self): | |
self.close_connection = True | |
ip = self.client_address[0] | |
if ip == '127.0.0.1' and 'X-Forwarded-For' in self.headers: | |
ip = self.headers['X-Forwarded-For'] | |
tokens = tokenbucket.acquire(ip) | |
debt = 128 - BURSTNESS - tokens | |
sys.stderr.write('%s has %d tokens with %d debt\n' % (ip, tokens, debt)) | |
if debt > 0: | |
if tokens < 60: | |
sys.stderr.write('banning %s\n' % (ip)) | |
tokenbucket.blackhole(ip) | |
self.bounce(debt) | |
return | |
content_length = int(self.headers['Content-Length']) | |
if content_length > 64 * 1024: | |
self.send_response(413) | |
self.send_header('Content-Type', 'text/plain; charset=utf-8') | |
self.send_header('Cache-Control', 'max-age=0, private, no-store'); | |
self.send_header('Connection', 'close') | |
self.end_headers() | |
self.wfile.write(b'*** PAYLOAD TOO LARGE ***\n') | |
self.wfile.flush() | |
post_data = self.rfile.read(content_length) | |
post_data = post_data.decode('utf-8') | |
if 'application/x-www-form-urlencoded' in self.headers['Content-Type']: | |
parameters = urllib.parse.parse_qs(post_data) | |
code = parameters['code'][0] if 'code' in parameters else '' | |
else: | |
code = post_data | |
with open(CODE_LOG, 'a') as f: | |
f.write('--- %s\n%s\n' % (ip, code)) | |
self.send_response(200) | |
self.send_header('Content-Type', 'text/plain; charset=utf-8') | |
self.send_header('Cache-Control', 'max-age=0, private, no-store'); | |
self.send_header('Connection', 'close') | |
self.end_headers() | |
self.wfile.flush() | |
os.close(10) # close blackhole client sock | |
os.dup2(4, 1) # map client socket to stdout | |
os.dup2(4, 2) # map client socket to stderr | |
supervise() | |
resource.setrlimit(resource.RLIMIT_FSIZE, FSZ_LIMIT) | |
resource.setrlimit(resource.RLIMIT_NPROC, THR_LIMIT) | |
cosmo.pledge(CLIENT_PLEDGE, None) # restrict permissible system calls | |
exec(code) | |
def bounce(self, debt): | |
self.send_response(429) | |
self.send_header('Content-Type', 'text/plain; charset=utf-8') | |
self.send_header('Cache-Control', 'max-age=0, no-store'); | |
self.send_header('Connection', 'close') | |
self.end_headers() | |
self.wfile.write(b'*** TOO MANY REQUESTS ***\n') | |
self.wfile.write(b'*** WAIT %d SECONDS ***\n' % (int(math.ceil(debt * REPLENISH)))) | |
self.wfile.flush() | |
def supervise(): | |
global pid | |
signal.setitimer(signal.ITIMER_REAL, TIM_LIMIT) | |
started = time.time() | |
pid = os.fork() | |
if not pid: | |
return | |
pid, ws, ru = os.wait4(pid, 0) | |
if os.WIFEXITED(ws): | |
print('*** EXITED WITH STATUS %d ***' % (os.WEXITSTATUS(ws))) | |
else: | |
print('*** TERMINATED BY %s ***' % (signal.Signals(os.WTERMSIG(ws)).name)) | |
cputime = ru.ru_utime + ru.ru_stime | |
print('*** USED %.6f SECONDS OF WALL TIME ***' % (time.time() - started)) | |
print('*** USED %.6f SECONDS OF CPU TIME (%.3f%% SYSTEM) ***' % | |
(cputime, ru.ru_stime / cputime * 100)) | |
print('*** BALLOONED TO %d KB OF RESIDENT MEMORY ***' % (ru.ru_maxrss)) | |
if ru.ru_msgrcv or ru.ru_msgsnd: | |
print('*** RECEIVED %d MESSAGES AND SENT %d ***' % | |
(ru.ru_msgrcv, ru.ru_msgsnd)) | |
if ru.ru_nvcsw or ru.ru_nivcsw: | |
print('*** TRIGGERED %d CONTEXT SWITCHES ***' % | |
(ru.ru_nvcsw + ru.ru_nivcsw)) | |
print('*** TRIGGERED %d PAGE FAULTS ***' % | |
(ru.ru_minflt + ru.ru_majflt)) | |
if ru.ru_nsignals: | |
print('*** RECEIVED %d SIGNALS ***' % (ru.ru_nsignals)) | |
os._exit(0) | |
if __name__ == '__main__': | |
with open(sys.argv[0], 'rb') as fin: | |
SAUCE = fin.read().replace(b'&', b'&').replace(b'<', b'<').replace(b'>', b'>') | |
if not os.path.isdir('/public'): | |
run('sudo mkdir /public') | |
run('sudo chmod 1777 /public') | |
if tokenbucket.blackhole('0.0.0.0') != 0: | |
sys.stderr.write("warning: blackholed.com isn't running\n") | |
tokenbucket.config(REPLENISH, BUCKETBIT) | |
resource.setrlimit(resource.RLIMIT_NOFILE, FDS_LIMIT) | |
signal.signal(signal.SIGALRM, on_sigalrm) | |
signal.signal(signal.SIGXCPU, on_sigxcpu) | |
signal.signal(signal.SIGXFSZ, on_sigxfsz) | |
cosmo.pledge(None, None) # throws exception if seccomp isn't available | |
cosmo.unveil('', None) # throws exception if landlock isn't available | |
cosmo.pledge(SERVER_PLEDGE, None) # server system call restrictions | |
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 | |
addr = ('0.0.0.0', port) | |
print('pledgethon http://%s:%d' % (addr[0], addr[1])) | |
httpd = Server(addr, Handler) | |
httpd.serve_forever() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment