Skip to content

Instantly share code, notes, and snippets.

@jart
Created December 8, 2023 06:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jart/f79f98dd06a6f2413a775c5d39948176 to your computer and use it in GitHub Desktop.
Save jart/f79f98dd06a6f2413a775c5d39948176 to your computer and use it in GitHub Desktop.
# 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'&amp;').replace(b'<', b'&lt;').replace(b'>', b'&gt;')
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