This is CS50 Sandbox.
CS50 Sandbox is a sandbox in which untrusted code can be executed. It supports these operations:
Installation
Amazon EC2
- Spawn an
c1.mediuminstance of AMI ami-08d97e61 (which is 32-bit Fedora 17), per http://fedoraproject.org/wiki/Cloud_images. sudo su -yum -y install http://mirror.cs50.net/appliance50/17/i386/RPMS/library50-c-4-1.i386.rpmyum -y install http://mirror.cs50.net/sandbox50/sandbox50-0-22.i386.rpmgit clone git@github.com:cs50/run50.git /opt/sandbox50/public/run50- Visit http://w.x.y.z:8080/run50/ to use CS50 Run, where
w.x.y.zis instance's IP address.
CS50 Appliance
- Install CS50 Appliance 17.
sudo yum -y update appliance50rebootsudo suyum -y install http://mirror.cs50.net/sandbox50/sandbox50-0-22.i386.rpmgit clone git@github.com:cs50/run50.git /opt/sandbox50/public/run50- Visit http://w.x.y.z:8080/run50/ to use CS50 Run, where
w.x.y.zis appliance's IP address.
*To implement a series of checks with an identifier of foo, create a text file called
/usr/share/sandbox50/checks/foo.js or /usr/share/sandbox50/checks/foo/index.js.
See /usr/share/sandbox50/checks/ for examples. See implementation details
for documentation. Use http://w.x.y.z:8080/examples/check.html to experiment with CS50 Check.
Usage
For actual examples, see public/examples/.
check
To run a series of checks against a previously uploaded directory of files, POST to /check
with a Content-Type of application/json a JSON object of the form
{
"checks": String,
"sandbox": { "homedir": String }
}
where checks is the unique identifier for those checks and
sandbox defines a "sandbox" to use for the command's execution, where homedir is
the unique identifier for a previously uploaded directory of files, a copy of which
will be mounted as $HOME for each of the checks in the series.
Upon successful execution, the server will return a JSON object of the form
{
"results": Object
}
where results is an object, each of whose keys is the unique identifier for a
check in the series, the value of which is an object of the form
{
"dependencies": [String, ...],
"description": String,
"result": Object,
"script": [Object, ...]
}
where dependencies is an array, each of whose elements is the unique identifier for another check
on which the check depends, description is a human-friendly description of the check, result
is either
-
true, which signifies that the check passed, -
null, which signifies that the check was not executed because of a failed or erred dependency, or -
false, which signifies that the check failed,
and script is an array of objects, each of the form
{
"actual": { type: String, value: String },
"expected": { type: String, value: String }
}
where expected describes what was expected, and actual describes what actually occurred,
if anything, up until the point when the check passed or failed.
For instance, this response indicates that two checks passed:
{
"results": {
"compiles": {
"dependencies": [],
"description": "Does file.c compile?",
result: true
},
"runs": {
"dependencies": [
"compiles"
],
"description": "Does a.out run?",
"result": true
}
}
}
By contrast, this response indicates that one check passed whereas another check failed (because of an unexpected exit code):
{
"results": {
"compiles": {
"dependencies": [],
"description": "Does file.c compile?",
"result": true
},
"runs": {
"dependencies": [
"compiles"
],
"description": "Does a.out exit with 0?",
"result": false,
"script": [
{
"actual": {
"type": "exit",
"value": 1
},
"expected": {
"type": "exit",
"value": 0
}
}
]
}
}
}
Similarly does this response indicate that one check passed whereas another check failed (because of unexpected standard output):
{
"results": {
"compiles": {
"dependencies": [],
"description": "Does file.c compile?",
"result": true
},
"runs": {
"dependencies": [
"compiles"
],
"description": "Does a.out print \"foo\"?",
"result": false,
"script": [
{
"actual": {
"type": "stdout",
"value": "bar"
},
"expected": {
"type": "stdout",
"value": "foo"
}
}
]
}
}
}
And this response indicates that two checks failed, one of which wasn't even executed because of its dependency on the other:
{
"results": {
"compiles": {
"dependencies": [],
"description": "Does file.c compile?",
"result": false,
"script": [
{
"actual": {
"type": "exit",
"value": 1
},
"expected": {
"type": "exit",
"value": 0
}
}
]
},
"runs": {
"dependencies": [
"compiles"
],
"description": "Does a.out run?",
"result": null,
"script": []
}
}
}
Upon any error that prevents execution of checks altogether, the server will instead return a JSON object of the form
{
"error": Object
}
where error is an error object, with an HTTP status code of 400.
Upon any error that prevents execution of a particular check, the server will return a key of
error, whose value is an explanatory string, instead of a key of result for that check.
run
To run commands inside of CS50 Sandbox, you may use HTTP POST or socket.io.
POST
To run a command inside of CS50 Sandbox via POST, POST to /run
with a Content-Type of application/json a JSON object of the form
{
"cmd": String,
"sandbox": { "homedir": String }
}
where cmd is the command to be executed (including any command-line arguments) and
sandbox defines a "sandbox" to use for the command's execution, where homedir
is the unique identifier for a previously uploaded directory of files, a copy of which
will be mounted as $HOME.
Upon successful execution, the server will return a JSON object of the form
{
"code": Number,
"sandbox": String,
"script": String,
"stderr": String,
"stdout": String
}
where code is the command's exit code, sandbox is a unique identifier
for the sandbox in which the command was executed (in which subsequent commands
could also be run), script is the command's terminal output (with standard error
and standard output interwoven), stderr is the command's standard error, and
stdout is the command's standard output, with an HTTP status code of 200.
To run a subsequent command inside of the same sandbox, POST to run
with a Content-Type of application/json a JSON object of the form
{
"cmd": String,
"sandbox": String
}
where cmd is the subsequent command to be executed (including any command-line arguments)
and sandbox is the unique identifier for the sandbox in which the previous command
was executed.
Upon any error, the server will instead return a JSON object of the form
{
"error": Object
}
where error is an error object, with an HTTP status
code of 400.
socket.io
To run a command inside of CS50 Sandbox via socket.io, open a connection as follows
var socket = io.connect('https://run.cs50.net', { 'force new connection': true });
where run.cs50.net is CS50 Sandbox's hostname, and then emit a run event, passing
in a JSON object of the form
{
"cmd": String,
"sandbox": { "homedir": String }
}
where cmd is the command to be executed (including any command-line arguments) and
sandbox defines a "sandbox" to use for the command's execution, where homedir
is the unique identifier for a previously uploaded directory of files, a copy of which
will be mounted as $HOME.
If the executing command is waiting for standard input, the server will emit a stdin
event with no payload.
To send standard input to the executing command, emit a stdin event
whose payload is a String. To send EOF, emit an EOF event with
no payload. To send SIGINT, emit a SIGINT event with no payload.
If the command's execution produces standard error, it will be emitted
by a stderr event whose payload is a String. If the command's execution
produces standard output, it will be emitted by a stdout event whose
payload is a String.
Upon success execution, the server will emit an exit event whose
payload is a JSON object of the form
{
"code": Number,
"sandbox": String,
"script": script,
"stderr": String,
"stdout": String
}
where code is the command's exit code, sandbox is a unique identifier
for the sandbox in which the previous command was run (in which subsequent commands
could also be run), script is the command's terminal I/O interwoven (with standard
error and standard output interwoven), stderr is the command's standard error, and
stdout is the command's standard output. (Even though stderr and
stdout are also emitted by separate events, they are included in
the server's response for convenience, in case the command's execution
produced standard error or standard output before erring altogether.)
To run a subsequent command inside of CS50 Sandbox via socket.io, open a new connection as follows
var socket = io.connect('https://run.cs50.net', { 'force new connection': true });
where run.cs50.net is CS50 Sandbox's hostname, and then emit a run event, passing
in a JSON object of the form
{
"cmd": String,
"sandbox": String
}
where cmd is the subsequent command to be executed (including any command-line arguments)
and sandbox is the unique identifier for the sandbox in which the previous command
was executed.
Upon any error, the server will instead emit an error event whose payload is a JSON object of the form
{
"error": Object
}
where error is an error object.
save
To trigger a browser to save an HTML textarea (or other String) locally
as a file, POST to /save with a Content-Type of
application/x-www-form-urlencoded a single key/value pair, where the
key is the file's name and the value is the file's contents.
The server's response will be of type application/octet-stream, thereby
triggering a file to be saved locally.
Upon error, the server will instead return a JSON object of the form
{
"error": Object
}
where error is an error object, with an HTTP status
code of 400.
upload
To upload files to CS50 Sandbox, POST to /upload with a Content-Type
of application/x-www-form-urlencoded or multipart/form-data.
Each key/value pair POSTed will be saved server-side as a file, whereby
the file's name will be the key and the file's contents will be the value,
the latter of is treated as a file in the case of multipart/form-data
(as though it were an HTML input for which type="file") or a String
in the case of application/x-www-form-urlencoded (as though it were an HTML
textarea or an HTML input for which type="text").
{
"id": String,
"uploads": [String, ...]
}
where uploads is an array of filenames saved and id is a unique identifer
for those uploads' parent directory, with an HTTP status code of 200.
Upon error, the server will instead return a JSON object of the form
{
"error": Object
}
where error is an error object, with an HTTP status
code of 400.
Errors
Upon any error, the server will either return (if using Ajax) a JSON object of the form
{
"error": { "code": String, "message": String }
}
or emit (if using socket.io) an error event whose payload is a JSON object of the same form,
where code is any of
-
'E_CONFIG', which signifies that there's a server-side configuration error -
'E_EXCESS', which signifies thatcmdgenerated excessive standard output or standard error -
'E_KILLED', which signifies thatcmdwas killed (as via SIGINT) -
'E_SIZE', which signifies that an API call contained too many bytes -
'E_TIMEOUT', which signifies thatcmdtook too long to execute -
'E_UNKNOWN', which signifies an error (currently) without a unique idenfier -
'E_USAGE', which signifies incorrect usage of an API
and message, if present, is an explanatory string.
Hosting
AWS
These instructions arbitrarily assume an availability zone of **us-east-1*.*
- Visit https://console.aws.amazon.com/vpc/home?region=us-east-1#s=Home.
- Click Create another VPC or Get started creating a VPC.
- Select VPC with Public and Private Subnets, then click Continue.
- Specify the below, then click Create VPC.
-
One VPC with an Internet Gateway
- IP CIDR block: 10.0.0.0/16
-
2 Subnets
- Public Subnet: 10.0.0.0/24
- Availability Zone: us-east-1a
- Private Subnet: 10.0.1.0/24
- Availability Zone: us-east-1a
-
One NAT Instance with an Elastic IP Address
- Instance Type: m1.small
- Key Pair Name: up to you
-
Hardware Tenancy
- Tenancy: Default
-
One VPC with an Internet Gateway
- Once "Your VPC has been successfully created", click Close.
- Visit https://console.aws.amazon.com/vpc/home?region=us-east-1#s=vpcs and take note of the VPC's VPC ID.
- Visit https://console.aws.amazon.com/ec2/home?region=us-east-1&#s=Instances, find the m1.small instance that was launched in the VPC's public subnet, and assign it a Name of run-us-east-1a.
- Visit https://console.aws.amazon.com/ec2/home?region=us-east-1&#s=Home.
- Click Launch Instance.
- Select Classic Wizard, then click Continue.
- Select Community AMIs, then search for ami-08d97e61 (per http://fedoraproject.org/wiki/Cloud_images), then click Select.
- Specify the below, then click Continue.
- Number of Instances: 2
- Instance Type: High-CPU Medium (c1.medium, 1.7 GB)
- Launch into: VPC
- Subnet: 10.0.1.0/24 (*i.e.*, the private subnet)
- Specify the below, then click Continue.
- Kernel ID: Use Default
- RAM Disk ID: Use Default
- Monitoring: unchecked
- User Data: blank
- Termination Protection: checked
- Shutdown Behavior: Stop
- IAM Role: None
- Tenancy: Default
- Number of Network Interfaces: 1 with defaults
- Specify the below, then click Continue.
- Name: sandbox50
- Select a key pair, then click Continue.
- Select or create a security group that allows inbound TCP 22 (SSH), TCP 80 (HTTP), TCP 443 (HTTPS), and TCP 8080, then click Continue.
- Click Launch.
- Visit https://console.aws.amazon.com/ec2/home?region=us-east-1&#s=Instances, find the m1.small instance that was launched in the VPC's public subnet, and change its security group to run (as by right-clicking it).
- Visit https://console.aws.amazon.com/ec2/home?region=us-east-1&#s=LoadBalancers.
- Click Create a Load Balancer.
- Specify the below, then click Continue:
- Load Balancer Name: sandbox50-us-east-1
- Create LB inside: 10.0.0.0/16 (*i.e.*, VPC)
-
Listener Configuration:
- HTTP | 80 | HTTP | 8080
- Specify the below, then click Continue:
- Ping Protocol: HTTP
- Ping Port: 8080
- Ping Path: /status
- Response Timeout: 5 Seconds
- Health Check Interval: 0.1 Minutes
- Unhealthy Threshold: 2
- Healthy Threshold: 2
- Add 10.0.0.0/24 (*i.e.*, public subnet) to Selected Subnets, then click Continue.
- Select same security group as before, then click Continue.
- Check both sandbox50 instances, then click Continue.
- Click Create.
- Click Close.
- Visit https://console.aws.amazon.com/ec2/home?region=us-east-1&#s=LoadBalancers.
- Take note of the DNS Name of the load balancer and create a CNAME (*e.g.*, run.cs50.net) that resolves to it.
SSH to each sandbox50 instance, disable iptables.
Implementation Details
Checks
By default, checks live in /usr/share/sandbox50/checks. If the unique identifier for a series
of checks is foo, then CS50 Sandbox will look for those checks in /usr/share/sandbox50/checks/foo.js
or, if not found there, in /usr/share/sandbox50/checks/foo/index.js. Forward slashes are treated as path
separators, so if the unique identifier for a series of checks is foo/bar, CS50 Sandbox
will look for those checks in /usr/share/sandbox50/checks/foo/bar.js or, if not found there,
in /usr/share/sandbox50/checks/foo/bar/ind
.
A series of checks, meanwhile, is implemented as a node module in which module.exports is assigned
an Object, each of whose keys is a unique identifier for a check. The value of each of those keys is
either a Function or an Array, the last of whose elements is a Function, the rest of whose elements
are the unique identifiers for other checks on which the check depends.
Each check must return a chain by calling a function called check (whose only argument
is a String that describes the check), followed by one or more "links."
For instance, this module contains a series of two checks, the latter of which depends on the former (in which case it will only be executed if the former passes):
module.exports = {
compiles: function(check) {
return check('Does src.c compile?')
.run('clang src.c -lcs50')
.exit(0);
},
runs: ['compiles', function(check) {
return check('Does a.out run properly?')
.run('clang src.c -lcs50')
.exit(0)
.run('./a.out')
.stdout('hello, world\n')
.exit(0);
}]
};
Note that, even though runs depends on compiles, it must still compile src.c itself,
since each check is run in isolation.
Supported links include:
-
.cp(src, dst), wheresrcanddst, each of typeString, prescribe the source and destination, respectively, of a file or directory to be copied forcibly and recursively (wherebysrccan be found in the same directory as the series of checks anddstcan or wil be found in the sandbox) -
.diff(expected, actual), whereexpectedandactual, each of typeString, prescribe files that should be compared (wherebyexpectedcan be found in the same directory as the series of checks andactualcan be found in the sandbox) -
.exists(expected), whereexpectedis aStringthat prescribes the name of a file that's expected to exist in the sandbox -
.exit(code), wherecodeis- an integral
Number, which prescribes the expected exit code of the most recentcmdin the chain -
undefined, which matches any exit code
- an integral
-
patch(originalfile, patchfile), which patchesoriginalfilewithpatchfile(wherebyoriginalfileis expected to exist in the sandbox andpatchfilecan be found in the same directory as the series of checks) -
.run(cmd), wherecmdis aStringthat prescribes a command to be executed (including any command-line arguments) -
.stderr(expected), whereexpectedis- a
RegExpthat must match standard error from the most recentcmdin the chain - the name of a file, whose contents must match standard error from the most recent
cmdin the chain -
undefined, which matches any standard error from the most recentcmdin the chain
- a
-
.stdin(s), wheresis- a
String, which specifies that the most recentcmdin the chain is expected to block for standard input, at which point theStringwill be piped intocmd -
undefined, which specifies that the most recentcmdin the chain is expected to block for standard input
- a
-
.stdout(expected), whereexpectedis- a
RegExpthat must match standard output from the most recentcmdin the chain - the name of a file, whose contents must match standard output from the most recent
cmdin the chain -
undefined, which matches any standard output from the most recentcmdin the chain
- a
Files
/etc/php.ini
short_open_tag = On
...
date.timezone = America/New_York
Troubleshooting
Error: EACCES, Permission denied
/opt/sandbox50/node_modules/forever/node_modules/daemon/lib/daemon.js:34
: binding.start(stdout);
^
Error: EACCES, Permission denied
at Object.start (/opt/sandbox50/node_modules/forever/node_modules/daemon/lib/daemon.js:34:15)
at Object.startDaemon (/opt/sandbox50/node_modules/forever/lib/forever.js:381:16)
at /opt/sandbox50/node_modules/forever/lib/forever/cli.js:229:27
at /opt/sandbox50/node_modules/forever/lib/forever/cli.js:134:5
at Object.oncomplete (/opt/sandbox50/node_modules/forever/lib/forever.js:342:24)
If seeing this error, odds are you've run index.js as someone other than sandbox50. To resolve:
rm -r /var/sandbox50/*
rm -r /var/log/sandbox50/*
References
- git
- jQuery
- Node.js
- socket.io
- Underscore.js