├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE.txt ├── README.md ├── benchmarks ├── README.md ├── node-hello-world-cluster.js ├── node-hello-world.js ├── osgood-hello-world-app.js └── osgood-hello-world-worker.js ├── build.rs ├── docs ├── Building.md ├── Command-Line-Usage.md ├── CouchDB-CRUD-App.md ├── GitHub-API-Merge-App.md ├── Hello-World-App.md ├── Home.md ├── Installation.md ├── JavaScript-Features.md ├── Osgood-Application-File.md ├── Osgood-Overview.md └── Osgood-Worker-Files.md ├── examples ├── contact │ ├── app.js │ ├── contact.js │ └── static │ │ ├── index.html │ │ ├── scripts │ │ └── contact.js │ │ └── styles │ │ └── main.css ├── couchdb-rest │ ├── README.md │ ├── app.js │ ├── commands.sh │ ├── common.js │ ├── create.js │ ├── delete.js │ ├── list.js │ ├── update.js │ └── view.js └── simple │ ├── Dockerfile │ ├── README.md │ ├── app.js │ ├── gh-merge.js │ └── hello.js ├── js ├── bootstrap │ ├── base64.js │ ├── body_mixin.js │ ├── common.js │ ├── console.js │ ├── context.js │ ├── fetch.js │ ├── form_data.js │ ├── headers.js │ ├── inbound.js │ ├── index.js │ ├── request.js │ ├── response.js │ └── timers.js ├── config_bootstrap.js ├── package-lock.json ├── package.json ├── preamble.js └── webpack.config.js ├── osgood-v8-macros ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests │ └── smoke.rs ├── osgood-v8 ├── Cargo.toml ├── build.rs ├── src │ ├── binding.rs │ ├── lib.rs │ ├── wrapper.cpp │ └── wrapper │ │ ├── array.rs │ │ ├── array_buffer.rs │ │ ├── context.rs │ │ ├── exception.rs │ │ ├── function.rs │ │ ├── functioncallbackinfo.rs │ │ ├── handle_scope.rs │ │ ├── isolate.rs │ │ ├── local.rs │ │ ├── mod.rs │ │ ├── module.rs │ │ ├── number.rs │ │ ├── object.rs │ │ ├── private.rs │ │ ├── script.rs │ │ ├── string.rs │ │ └── utf8value.rs └── v8-version.txt ├── osgood.svg ├── scripts └── install-v8 ├── src ├── config.rs ├── main.rs ├── worker.rs └── worker │ ├── fetch.rs │ ├── headers.rs │ ├── inbound.rs │ ├── internal.rs │ ├── logging.rs │ ├── policies.rs │ └── timers.rs └── tests ├── integration ├── README.md ├── basic-app │ ├── app.js │ ├── bad-protocol.js │ ├── badhandler.js │ ├── badpolicy.js │ ├── badstart.js │ ├── complex-bad.js │ ├── complex-good.js │ ├── connection-refused.js │ ├── constants.js │ ├── echo-headers.js │ ├── evil.js │ ├── fetches.js │ ├── hello.js │ ├── host-header-foolery.js │ ├── http-policies.js │ ├── image.js │ ├── imports.js │ ├── imports │ │ ├── import1.js │ │ └── import2.js │ ├── intrinsic.js │ ├── multipart.js │ ├── nohandler.js │ ├── noreply.js │ ├── poststream.js │ ├── relative.js │ ├── responses.js │ ├── return-array.js │ ├── return-class-instance.js │ ├── string-stream-resp.js │ ├── syntaxerror.js │ ├── tests │ │ ├── basic.js │ │ └── error-cases.js │ ├── url-params.js │ └── urlencode.js ├── common.js ├── server │ ├── node-test-server.js │ ├── package-lock.json │ └── package.json ├── static │ ├── app-static-four.js │ ├── app-static-three.js │ ├── app-static-two.js │ ├── app-static.js │ ├── files-two │ │ ├── cake.html │ │ └── index.html │ ├── files │ │ ├── desserts │ │ │ ├── cake.html │ │ │ └── index.html │ │ ├── drinks │ │ │ ├── orange.html │ │ │ └── soft │ │ │ │ ├── cola.html │ │ │ │ ├── orange.html │ │ │ │ └── style.css │ │ ├── fruits.html │ │ ├── fruits │ │ │ ├── default.html │ │ │ └── orange.html │ │ ├── hello.css │ │ ├── images │ │ │ ├── blue.png │ │ │ ├── brown.jpg │ │ │ ├── hello-world.pdf │ │ │ └── slate.svg │ │ ├── index.html │ │ ├── things.html │ │ └── veggies │ │ │ ├── celery.html │ │ │ └── index.html │ └── tests │ │ ├── clean-url-no-index.js │ │ ├── clean-url.js │ │ ├── no-index.js │ │ └── rename-index.js └── test └── test.rs /.gitignore: -------------------------------------------------------------------------------- 1 | js/dist 2 | js/node_modules 3 | tests/integration/server/node_modules 4 | js/vendor 5 | target 6 | bin 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: stable 3 | 4 | os: 5 | - linux 6 | - osx 7 | 8 | env: 9 | - CUSTOM_V8=$HOME/v8 10 | 11 | addons: 12 | apt: 13 | packages: 14 | build-essential 15 | pkg-config 16 | libc++-dev 17 | libc++abi-dev 18 | clang 19 | libclang-dev 20 | libssl-dev 21 | 22 | before_script: 23 | - rustup component add rustfmt 24 | - nvm install 12 25 | 26 | script: 27 | - scripts/install-v8 28 | - pushd js && npm install && popd 29 | - cargo test 30 | 31 | before_deploy: 32 | - cargo build --release 33 | - zip -j osgood-$TRAVIS_OS_NAME-$TRAVIS_TAG.zip target/release/osgood 34 | 35 | deploy: 36 | provider: releases 37 | api_key: 38 | secure: YC1y83rWoiNOyRSzXZIjDKU4xkzMhe8XAy/fy2plEtIKF2JaoMGVzoUCBJF/7ahnEi5VCXCAvBx9gsG8TKUomG7LVRLaMusKGyCIioogWWvz7DQQH8BlVKQUV1nlbc9RcJqBSV4qatyDVpgCNgQlEFRWdeoxD8sAcxWOPhTMfMAXV5/JzReNCFn/oL2EWWKC8fKFkEKv5IyknZLt7rtDzzkz7/X0TW7YGNZSXkV2l2Nzf2gUzsweDVLhWvCWKzT56uQdOYCZPHonoNMqv95aEZQv1QUEURQSbfk2lGijG97yE09veD3yXfl287YZlHOLQsEnfHTCjo0pVdxviZwrYxKwopvKn4JlLIzr6V1hsblvmiD0sA4odMzcDItyYmzx343AmF82ziYgvVl17O+y8GWQaOnxbbmAjCeL4EoiOV8doOAz8IhIgvzoLkYddj+GnAniWQA/2LYCddhm/HvcVn3/8Kww8+jjMhxhgFOuOLvjMWIN94zmpM+KzayL7Ge7H4wkX0mjT1Riv5oDaGoe83Hm0qIfRJBrNhhLMc3bqxHXcFqfkKPObTABdevU7BDqzI6LucjVLwGhsRhWyCH4h/tMzhqDknZc7MKr3B2DPbvlW5x0DH02ppUX2V4uJ3aEO+Oq4DpGnMtOImpSK5CWhZDl1mX6IqITp7SKFs4bnLk= 39 | file: osgood-$TRAVIS_OS_NAME-$TRAVIS_TAG.zip 40 | skip_cleanup: true 41 | on: 42 | tags: true 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at hello@intrinsic.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Pull requests and issues are welcome and encouraged! Please use the templates 4 | when provided and follow the commit message guidelines and the Code of Conduct. 5 | 6 | ## Code of Conduct 7 | 8 | Osgood has a [Code of Conduct](./CODE_OF_CONDUCT) to which all contributors must 9 | adhere. 10 | 11 | ## Commit Message Guidelines 12 | 13 | Please use the [Node.js commit message 14 | guidelines](https://github.com/nodejs/node/blob/master/doc/guides/contributing/pull-requests.md#commit-message-guidelines), 15 | with the addition that the subsystem prefix is optional. 16 | 17 | ## Amending Pull Requests 18 | 19 | Rather than force-pushing, please submit commits called (or prefixed with) 20 | `[fixup]`. This way we can review the changes between revisions of your 21 | submission. We'll squash the commits on merge, taking the last non-trivial 22 | commit message. It's okay to force-push if the commit message is the only thing 23 | changing. 24 | 25 | ## Developer Certificate of Origin 26 | 27 | Osgood uses the [Developer Certificate of Origin](https://developercertificate.org/). 28 | 29 | ``` 30 | Developer Certificate of Origin 31 | Version 1.1 32 | 33 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 34 | 1 Letterman Drive 35 | Suite D4700 36 | San Francisco, CA, 94129 37 | 38 | Everyone is permitted to copy and distribute verbatim copies of this 39 | license document, but changing it is not allowed. 40 | 41 | 42 | Developer's Certificate of Origin 1.1 43 | 44 | By making a contribution to this project, I certify that: 45 | 46 | (a) The contribution was created in whole or in part by me and I 47 | have the right to submit it under the open source license 48 | indicated in the file; or 49 | 50 | (b) The contribution is based upon previous work that, to the best 51 | of my knowledge, is covered under an appropriate open source 52 | license and I have the right under that license to submit that 53 | work with modifications, whether created in whole or in part 54 | by me, under the same open source license (unless I am 55 | permitted to submit under a different license), as indicated 56 | in the file; or 57 | 58 | (c) The contribution was provided directly to me by some other 59 | person who certified (a), (b) or (c) and I have not modified 60 | it. 61 | 62 | (d) I understand and agree that this project and the contribution 63 | are public and that a record of the contribution (including all 64 | personal information I submit with it, including my sign-off) is 65 | maintained indefinitely and may be redistributed consistent with 66 | this project or the open source license(s) involved. 67 | ``` 68 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "osgood" 3 | version = "0.2.1" 4 | authors = ["Intrinsic "] 5 | edition = "2018" 6 | build = "build.rs" 7 | 8 | [build-dependencies] 9 | phf_codegen = "0.7.24" 10 | 11 | [dependencies] 12 | ansi_term = "0.11.0" 13 | clap = "2.33.0" 14 | futures = "0.1.25" 15 | glob = "0.3.0" 16 | hyper = "0.12" 17 | hyper-tls = "0.3.2" 18 | hyper-staticfile = "0.3.1" 19 | mime_guess = "1.8.7" 20 | num_cpus = "1.10.1" 21 | lazy_static = "1.3.0" 22 | libc = "0.2.51" 23 | log = "0.4.6" 24 | path-clean = "0.1.0" 25 | phf = "0.7.24" 26 | pretty_env_logger = "0.3.0" 27 | tokio = "0.1.18" 28 | tokio-threadpool = "0.1.15" 29 | url = "1.7.2" 30 | osgood-v8 = { path = "osgood-v8" } 31 | osgood-v8-macros = { path = "osgood-v8-macros" } 32 | 33 | [workspace] 34 | members = ["osgood-v8", "osgood-v8-macros"] 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # xenial is required due to the version of SSL that Osgood currently links against. 2 | # xenial is supported until April 2021: https://wiki.ubuntu.com/XenialXerus/ReleaseNotes#Support_lifespan 3 | FROM ubuntu:xenial 4 | 5 | ENV OSGOOD_VERSION 0.2.1 6 | ENV OSGOOD_SHA 20be5e5d78a6a92e18b03847b6249e374102580e54b502616776944842cb17f0 7 | 8 | RUN set -eux; \ 9 | groupadd -r -g 1000 osgood; \ 10 | useradd -r -g osgood -u 1000 osgood; \ 11 | apt-get update; \ 12 | apt-get install -y --no-install-recommends \ 13 | ca-certificates \ 14 | wget \ 15 | unzip \ 16 | libssl1.0.2 \ 17 | ; \ 18 | wget -O osgood.zip "https://github.com/IntrinsicLabs/osgood/releases/download/$OSGOOD_VERSION/osgood-linux-$OSGOOD_VERSION.zip"; \ 19 | echo "$OSGOOD_SHA osgood.zip" | sha256sum -c -; \ 20 | unzip osgood.zip; \ 21 | chmod +x ./osgood; \ 22 | mv ./osgood /usr/bin/osgood; \ 23 | rm osgood.zip; \ 24 | mkdir -p /srv/osgood; \ 25 | chown osgood:osgood /srv/osgood 26 | 27 | # Copies a tiny sample app 28 | # COPY ./examples/simple/* /srv/osgood-example/ 29 | VOLUME /srv/osgood 30 | 31 | WORKDIR /srv/osgood 32 | 33 | EXPOSE 8080 34 | 35 | # Users can override this when they extend the image 36 | # CMD ["osgood", "/srv/osgood-example/app.js"] 37 | ENTRYPOINT ["osgood"] 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 The Osgood authors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Osgood](./osgood.svg) 2 | 3 | [![Gitter](https://badges.gitter.im/osgoodplace/community.svg)](https://gitter.im/osgoodplace/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 4 | 5 | Osgood is a secure, fast, and simple platform for running JavaScript HTTP 6 | servers. It is written using Rust and V8. 7 | 8 | Services written today share a common flaw: Being over-privileged. Osgood is an 9 | attempt to build a platform from the ground up, one which applies the 10 | [_Principle of Least 11 | Privilege_](https://en.wikipedia.org/wiki/Principle_of_least_privilege) at its 12 | very core. Osgood requires that policies be written ahead of time describing 13 | the I/O requirements of an application. If such an operation hasn't been 14 | whitelisted, it will fail. Developers familiar with JavaScript development in 15 | the web browser should feel right at home with the APIs provided in Osgood. 16 | 17 | 18 | ## Documentation 19 | 20 | - [Osgood API Docs](https://github.com/IntrinsicLabs/osgood/wiki) 21 | - [Introducing Osgood (blog)](https://dev.to/tlhunter/introducing-osgood-4k1m) 22 | - [Hosting a Static Site and Contact Form with Osgood (blog)](https://dev.to/tlhunter/hosting-a-static-site-and-contact-form-with-osgood-5c1g) 23 | - [Osgood and CouchDB (blog)](https://dev.to/tlhunter/osgood-and-couchdb-125k) 24 | - [Introducing Osgood (presentation)](https://thomashunter.name/presentations/introducing-osgood/#/) 25 | 26 | 27 | ## Hello, World! 28 | 29 | ```javascript 30 | // app.js 31 | app.port = 3000; 32 | 33 | app.get('/hello', 'hello-worker.js'); 34 | ``` 35 | 36 | ```javascript 37 | // hello-worker.js 38 | export default () => 'Hello, World!'; 39 | ``` 40 | 41 | ```bash 42 | $ osgood app.js 43 | $ curl http://localhost:3000/hello 44 | ``` 45 | 46 | 47 | ## What is Osgood? 48 | 49 | Osgood is a JavaScript runtime purpose-built to run HTTP servers. Its goals are 50 | to provide a secure way to build HTTP servers that are fast and simple. Osgood 51 | handles server routing and configuration for you, allowing you to focus on 52 | application code. 53 | 54 | Today we build web applications with general purpose language runtimes. Osgood 55 | is an experiment that asks the question: "What if we built a runtime 56 | specifically for web apps? What kind of benefits can we get from being at 57 | a higher level of abstraction?" 58 | 59 | Since the Osgood runtime has intimate knowledge of the routing table we get the 60 | ability to isolate controllers for free (we refer to these as Workers). The I/O 61 | performed by the application, as well as policy enforcement, happens in 62 | Rust-land. Each worker has its own set of permissions. 63 | 64 | Here's an example policy: 65 | 66 | ```javascript 67 | policy.outboundHttp.allowGet('https://intrinsic.com'); 68 | ``` 69 | 70 | Consider the situation where Controller A has permission to send a message to 71 | `intrinsic.com`, and Controller B has access to user credentials. Within 72 | a properly configured Osgood application this means it's not possible to 73 | transmit user credentials to `intrinsic.com`. 74 | 75 | 76 | ## Installing Osgood 77 | 78 | ### Download a Prebuilt Release 79 | 80 | All prebuilt releases can be downloaded from the 81 | [Releases](https://github.com/IntrinsicLabs/osgood/releases) page. 82 | 83 | ### Building Osgood 84 | 85 | We have more information on compiling Osgood on our [Building 86 | Osgood](https://github.com/IntrinsicLabs/osgood/wiki/Building) wiki page. 87 | 88 | 89 | ## Osgood Overview 90 | 91 | ### Application File 92 | 93 | An Osgood application file is essentially the entrypoint for the application. 94 | Each application will have a single application file. It is the only necessary 95 | argument for the `osgood` command. 96 | 97 | This file has three purposes: 98 | 99 | - Configure global settings such as port and interface 100 | - Route incoming requests to the desired Osgood worker 101 | - Configure the security policies for each Osgood worker 102 | 103 | More information about Osgood application files are available on the [Osgood 104 | Application 105 | File](https://github.com/IntrinsicLabs/osgood/wiki/Osgood-Application-File) 106 | wiki page. 107 | 108 | 109 | ### Worker File 110 | 111 | An Osgood worker file works by exporting a default function. Typically you'll 112 | export an `async` function but it also works fine by returning a promise or a 113 | string value. 114 | 115 | Workers are called with information about the incoming request and the returned 116 | value is then used to dictate the response to the client. 117 | 118 | More information about Osgood worker files are available on the [Osgood Worker 119 | Files](https://github.com/IntrinsicLabs/osgood/wiki/Osgood-Worker-Files) wiki 120 | page. 121 | 122 | ## Contributing 123 | 124 | Contributions are welcome! Please see [`CONTRIBUTING.md`](./CONTRIBUTING.md). 125 | 126 | ## License 127 | 128 | Osgood uses the MIT License. Please see [`LICENSE.txt`](./LICENSE.txt). 129 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | When performing benchmarks it's important to make sure the conditions for 4 | running two separate servers are the same. One of these considerations is that 5 | the data being transmitted should be exactly equal. This requires checking the 6 | headers, for example by using `curl -i` or `httpie`. 7 | 8 | 9 | ## Included Services 10 | 11 | We've included a `Hello, World!` application for both Osgood, as well as 12 | Node.js. Both files make use of a shebang and can be run directory (assuming 13 | `node` and `osgood` are available in your path). 14 | 15 | Both applications will listen on port `3000` and will respond to a request made 16 | to `/hello`. 17 | 18 | 19 | ## Benchmark Command 20 | 21 | Here's the command we've been using while developing Osgood: 22 | 23 | ```sh 24 | siege -c 10 -r 100000 -b http://localhost:3000/hello 25 | ``` 26 | 27 | This command will terminate after a while and provide some results. It's 28 | important to test Osgood in this manner to prevent performance regressions from 29 | being released. 30 | -------------------------------------------------------------------------------- /benchmarks/node-hello-world-cluster.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.log(`Node.js ${process.version}`); 4 | 5 | const cluster = require('cluster'); 6 | const http = require('http'); 7 | const numCPUs = require('os').cpus().length; 8 | 9 | if (cluster.isMaster) { 10 | console.log(`Master ${process.pid} is running`); 11 | 12 | // Fork workers. 13 | for (let i = 0; i < numCPUs; i++) { 14 | cluster.fork(); 15 | } 16 | 17 | cluster.on('exit', (worker, code, signal) => { 18 | console.log(`worker ${worker.process.pid} died`); 19 | }); 20 | } else { 21 | // This can be benchmarked against the /hello endpoint 22 | // Both servers send the same number of bytes 23 | 24 | const hostname = '127.0.0.1'; 25 | const port = 3000; 26 | 27 | const server = http.createServer((req, res) => { 28 | res.statusCode = 200; 29 | res.setHeader('Content-Type', 'text/plain'); 30 | res.removeHeader('Connection'); 31 | res.end('Hello, world!'); 32 | }); 33 | 34 | server.listen(port, hostname, () => { 35 | console.log(`Server running at http://${hostname}:${port}/`); 36 | }); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /benchmarks/node-hello-world.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.log(`Node.js ${process.version}`); 4 | 5 | const http = require('http'); 6 | // This can be benchmarked against the /hello endpoint 7 | // Both servers send the same number of bytes 8 | 9 | const hostname = '127.0.0.1'; 10 | const port = 3000; 11 | 12 | const server = http.createServer((req, res) => { 13 | res.statusCode = 200; 14 | res.setHeader('Content-Type', 'text/plain'); 15 | res.removeHeader('Connection'); 16 | res.end('Hello, world!'); 17 | }); 18 | 19 | server.listen(port, hostname, () => { 20 | console.log(`Server running at http://${hostname}:${port}/`); 21 | }); 22 | -------------------------------------------------------------------------------- /benchmarks/osgood-hello-world-app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osgood 2 | // app.interface = '0.0.0.0'; -- default so commented it out 3 | app.port = 3000; 4 | //app.host = 'localhost'; -- default so commented it out 5 | 6 | app.get('/hello', 'osgood-hello-world-worker.js'); 7 | -------------------------------------------------------------------------------- /benchmarks/osgood-hello-world-worker.js: -------------------------------------------------------------------------------- 1 | console.log('Starting worker'); 2 | export default () => 'Hello, World!'; 3 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate phf_codegen; 2 | 3 | // use phf_codegen; 4 | 5 | use std::env; 6 | use std::ffi::OsStr; 7 | use std::fs; 8 | use std::io::{BufWriter, Write}; 9 | use std::path::Path; 10 | use std::process::Command; 11 | 12 | fn main() { 13 | let target_dir = env::var("OUT_DIR").unwrap(); 14 | if is_lock_newer_than_binary(target_dir) { 15 | println!("cargo:rerun-if-changed={}", "js/package-lock.json"); 16 | let child = Command::new("npm") 17 | .arg("install") 18 | .current_dir("js") 19 | .status() 20 | .unwrap(); 21 | assert!(child.success()); 22 | } 23 | 24 | let path = Path::new(&env::var("OUT_DIR").unwrap()).join("bootstrap.rs"); 25 | let mut file = BufWriter::new(fs::File::create(&path).unwrap()); 26 | 27 | write!( 28 | &mut file, 29 | "#[allow(clippy::all)]\nstatic BOOTSTRAP_MODULES: phf::Map<&'static str, &'static str> = " 30 | ) 31 | .unwrap(); 32 | 33 | let mut map = &mut phf_codegen::Map::::new(); 34 | 35 | let bootstrap_dir = Path::new("js/bootstrap"); 36 | assert!(bootstrap_dir.is_dir()); 37 | 38 | for entry in fs::read_dir(bootstrap_dir).unwrap() { 39 | let entry = entry.unwrap(); 40 | let path = entry.path(); 41 | // Ignore files without `.js` extension 42 | match path.extension().and_then(OsStr::to_str) { 43 | Some("js") => (), 44 | _ => continue, 45 | }; 46 | 47 | // Ignore files that start with `.` 48 | if path 49 | .file_name() 50 | .and_then(OsStr::to_str) 51 | .unwrap() 52 | .starts_with('.') 53 | { 54 | continue; 55 | } 56 | 57 | // TODO: Allow subdirs in bootstrap folder 58 | assert!(!path.is_dir()); 59 | println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); 60 | 61 | let key = String::from( 62 | path.strip_prefix(bootstrap_dir) 63 | .unwrap() 64 | .to_str() 65 | .unwrap() 66 | .clone(), 67 | ); 68 | map = map.entry( 69 | key, 70 | &format!("r#\"{}\"#", &(fs::read_to_string(&path).unwrap())), 71 | ); 72 | } 73 | 74 | map.build(&mut file).unwrap(); 75 | write!(&mut file, ";\n").unwrap(); 76 | } 77 | 78 | // This prevents `cargo build` from always running `npm install` 79 | fn is_lock_newer_than_binary(target_dir: String) -> bool { 80 | let lock_file = fs::metadata("js/package-lock.json"); 81 | if let Err(_err) = lock_file { 82 | return true; 83 | } 84 | let lock_file = lock_file.unwrap(); 85 | 86 | let binary_file = fs::metadata(format!("{}/osgood", target_dir)); 87 | // let binary_file = fs::metadata("target/debug/osgood"); 88 | if let Err(_err) = binary_file { 89 | return true; 90 | } 91 | let binary_file = binary_file.unwrap(); 92 | 93 | if let Ok(lock_time) = lock_file.modified() { 94 | if let Ok(binary_time) = binary_file.modified() { 95 | return lock_time > binary_time; 96 | } else { 97 | return true; 98 | } 99 | } else { 100 | return true; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /docs/Building.md: -------------------------------------------------------------------------------- 1 | Osgood can be built from source by cloning the repository and running the 2 | following commands. This will require that you first install Rust and Node.js 3 | on your machine. Note that these are _only_ required for building; they are 4 | unnecessary for _running_ Osgood. 5 | 6 | ## Install Rust 7 | 8 | Osgood is built using Rust. Either run the command below or visit 9 | the [Install Rust](https://www.rust-lang.org/tools/install) page for more 10 | details. 11 | 12 | ```bash 13 | $ curl https://sh.rustup.rs -sSf | sh 14 | ``` 15 | 16 | ## Install Node.js and npm 17 | 18 | Osgood rebuilds some familiar JavaScript APIs which are commonly provided in 19 | browser environments but aren't part of the core JavaScript language itself. 20 | Some of these APIs we've built ourselves. Others are provided via an npm 21 | package. 22 | 23 | Osgood therefore requires that such packages be downloaded during the build 24 | process. Once all necessary packages are present they're essentially 25 | concatenated together (via Webpack) and the resulting file is distributed with 26 | Osgood. 27 | 28 | Run the following command to get a modern version of Node.js and npm: 29 | 30 | ```shell 31 | $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash 32 | ``` 33 | 34 | ## Compile 35 | 36 | Run the following command to build Osgood: 37 | 38 | ```shell 39 | $ cargo build 40 | ``` 41 | 42 | You can compile the project by running `cargo build`. This command compiles V8, 43 | which will likely take ~10-30 minutes on your machine. If you'd like to see 44 | V8's progress, run `cargo build -vv` instead. 45 | 46 | To avoid unnecessary work, the build script will leverage any existing 47 | `depot_tools` package the machine already has, if the script can find the 48 | necessary commands in your `PATH`. 49 | 50 | The build script can optionally use a custom or precompiled version of V8. To 51 | tell the script to do this, set the `CUSTOM_V8` environment variable to the 52 | path to the V8 folder. 53 | 54 | ### Ubuntu Users 55 | 56 | There are a few packages you'll need before you'll be able to compile: 57 | 58 | ```sh 59 | sudo apt install build-essential pkg-config libc++-dev libc++abi-dev \ 60 | clang libclang-dev libssl-dev 61 | ``` 62 | 63 | ### Arch Linux Users 64 | 65 | Google's `depot_tools` project has known issues when running on Arch Linux due 66 | to assumptions the project makes about the version of Python referenced by the 67 | `python` binary. Fixing this requires manual intervention; consider installing 68 | `depot-tools-git` from the AUR. 69 | -------------------------------------------------------------------------------- /docs/Command-Line-Usage.md: -------------------------------------------------------------------------------- 1 | The most basic usage of Osgood requires that you run the `osgood` binary and 2 | pass in the path to an [Application File](Osgood-Application-File) as the only 3 | argument. 4 | 5 | ```sh 6 | $ osgood ./app.js 7 | ``` 8 | 9 | ## Command Line Flags 10 | 11 | Three basic flags are currently provided by Osgood: 12 | 13 | ```sh 14 | $ osgood --help # displays help message 15 | $ osgood --version # displays version number 16 | $ osgood --v8-help # displays V8 flags 17 | ``` 18 | 19 | Additional flags can be passed to the underlying V8 engine. To get a list of 20 | the possible flags first run the command with the `--v8-help` flag. The listed 21 | flags can be passed in by prefixing them with `--v8-`. For example, the 22 | `--max-old-space-size` flag can be passed in like so: 23 | 24 | ```sh 25 | $ osgood --v8-max-old-space-size=10 ./app.js 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/CouchDB-CRUD-App.md: -------------------------------------------------------------------------------- 1 | This application will allow us to build an interface in front of CouchDB. We do 2 | this to keep our clients from becoming too tightly coupled with the backend 3 | system. 4 | 5 | Full source code is available at 6 | [examples/couchdb-rest](https://github.com/IntrinsicLabs/osgood/tree/master/examples/couchdb-rest). 7 | Here we'll only be covering a single, complex route. 8 | 9 | ## Application File `app.js` 10 | 11 | The full application will use five different routes, which is a pretty common 12 | CRUD pattern. The five routes are to perform five actions: LIST, VIEW, UPDATE, 13 | DELETE, CREATE. However, we'll only show code for the UPDATE route since that's 14 | the most complicated. 15 | 16 | ```javascript 17 | app.interface = '127.0.0.1'; 18 | app.port = 8000; 19 | app.host = 'localhost'; 20 | 21 | app.get('/users', 'list.js', policy => { 22 | policy.outboundHttp.allowGet('http://localhost:5984/users/_all_docs'); 23 | }); 24 | 25 | app.get('/users/:id', 'view.js', policy => { 26 | policy.outboundHttp.allowGet('http://localhost:5984/users/*'); 27 | }); 28 | 29 | app.delete('/users/:id', 'delete.js', policy => { 30 | policy.outboundHttp.allowGet('http://localhost:5984/users/*'); 31 | policy.outboundHttp.allowDelete('http://localhost:5984/users/*'); 32 | }); 33 | 34 | app.post('/users', 'create.js', policy => { 35 | policy.outboundHttp.allowPost('http://localhost:5984/users'); 36 | }); 37 | 38 | app.put('/users/:id', 'update.js', policy => { 39 | policy.outboundHttp.allowGet('http://localhost:5984/users/*'); 40 | policy.outboundHttp.allowPut('http://localhost:5984/users/*'); 41 | }); 42 | ``` 43 | 44 | The above syntax for extracting route parameters is inspired by existing tools 45 | such as Express.js. Each route has a policy function which describes its 46 | capabilities. For example, our route for performing updates needs to both GET 47 | data from CouchDB, as well as PUT data to it. 48 | 49 | ## Common File `common.js` 50 | 51 | This file contains some common tools that we'll use within the different 52 | routes. 53 | 54 | For example, when building a JSON-speaking HTTP API, it's common to respond 55 | with JSON data. This requires that we also send headers describing the content 56 | as being JSON. We also want to be able to override status codes for conveying 57 | different errors. That's what the `json` function defined below does. 58 | 59 | We also have a function called `dbRequest`, which is a simple function for 60 | generating HTTP requests for our CouchDB server. We use this function to pass 61 | along authentication and set the appropriate request headers. 62 | 63 | ```javascript 64 | const AUTH = `Basic ${btoa('s3w_admin:hunter12')}`; 65 | 66 | export function json(obj, status = 200) { 67 | const headers = new Headers({ 68 | 'Content-Type': 'application/json' 69 | }); 70 | 71 | const body = JSON.stringify(obj); 72 | 73 | const response = new Response(body, { headers, status }); 74 | 75 | return response; 76 | } 77 | 78 | // Makes a request to CouchDB 79 | export function dbRequest(method = 'GET', path = '', body = '') { 80 | const options = { 81 | method, 82 | headers: { 83 | Authorization: AUTH 84 | } 85 | } 86 | 87 | if (body) { 88 | options.headers['Content-Type'] = 'application/json'; 89 | options.body = JSON.stringify(body); 90 | } 91 | 92 | return fetch(`http://localhost:5984/users/${path}`, options); 93 | } 94 | ``` 95 | 96 | ## Worker File `update.js` 97 | 98 | Here is where the code for our UPDATE route lives. Note that we're able to 99 | `import` and `export` from within files. In this case we're importing code from 100 | `common.js`, code which would naturally be shared among all routes. 101 | 102 | Since we're using an `async` function we get to prefix asynchronous operations 103 | with the `await` keyword. This means our worker code ends up being pretty 104 | simple. There's also a ton of error handling going on which is why there are so 105 | many early returns. 106 | 107 | We are building this application to prevent the client from directly accessing 108 | the CouchDB server. This is done for a few reasons: 109 | 110 | - Hide authentication from the client 111 | - Transform data into a common format 112 | - Enforce certain data requirements (non-writable `id`, `created` time, `modified` time) 113 | 114 | ```javascript 115 | import { dbRequest, json } from './common.js'; 116 | 117 | export default async function (request, context) { 118 | const id = context.params.id; 119 | 120 | if (!id) { 121 | return json({ error: 'INVALID_REQUEST' }, 400); 122 | } 123 | 124 | try { 125 | var record = await request.json(); 126 | } catch (e) { 127 | return json({error: 'CANNOT_PARSE'}, 401); 128 | } 129 | 130 | if ((record.id && record.id !== id) || (record._id && record._id !== id)) { 131 | return json({error: 'CANNOT_RENAME'}, 401); 132 | } 133 | 134 | if (record.created || record.updated) { 135 | return json({error: 'CANNOT_CHANGE_DATES'}, 401); 136 | } 137 | 138 | const existing_record = await dbRequest('GET', id); 139 | 140 | const existing_obj = await existing_record.json(); 141 | 142 | if (existing_obj.error && existing_obj.error === 'not_found') { 143 | return json({ error: 'NOT_FOUND' }, 404); 144 | } 145 | 146 | // WARNING: This isn't atomic 147 | 148 | const rev = existing_obj._rev; 149 | 150 | record._rev = rev; 151 | 152 | // retain existing created time 153 | record.created = existing_obj.created; 154 | record.updated = (new Date()).toISOString(); 155 | 156 | const update_payload = await dbRequest('PUT', id, record); 157 | 158 | const update_obj = await update_payload.json(); 159 | 160 | if (update_obj.error) { 161 | return json({ error: 'UNABLE_TO_UPDATE' }, 500); 162 | } 163 | 164 | delete record._rev; // hide implementation detail 165 | record.id = update_obj.id; 166 | 167 | return json(record); 168 | } 169 | ``` 170 | -------------------------------------------------------------------------------- /docs/GitHub-API-Merge-App.md: -------------------------------------------------------------------------------- 1 | This application makes two outbound requests, transforms the data into a 2 | simpler format, and responds with the combined request. This is often referred 3 | to as the API Facade pattern. 4 | 5 | ## Application File `app.js` 6 | 7 | This file only needs a single incoming route, `GET /merge/:username`. It will 8 | make two outgoing HTTP requests and so we whitelist those requests. 9 | 10 | ```javascript 11 | app.port = 3000; 12 | 13 | app.route('GET', '/merge/:username', 'gh-merge.js', policy => { 14 | policy.outboundHttp.allowGet('https://api.github.com/users/*/gists'); 15 | policy.outboundHttp.allowGet('https://api.github.com/users/*/repos'); 16 | }); 17 | ``` 18 | 19 | ## Command Line 20 | 21 | You'll be able to run the application by doing the following: 22 | 23 | ```shell 24 | $ osgood ./app.js 25 | $ curl http://localhost/merge/tlhunter 26 | ``` 27 | 28 | The `:username` segment in the URL will be extracted and provided to the 29 | application via `context.params.username`. 30 | 31 | ## Worker File `merge.js` 32 | 33 | This application will make two requests to the GitHub API to get a list of 34 | gists and a list of repos for the specified user. It will then sort the entries 35 | by popularity and strip out a bunch of redundant data. Finally it will reply 36 | with the subset of data. 37 | 38 | ```javascript 39 | const MAX_LIST = 3; 40 | 41 | export default async function (request, context) { 42 | const username = context.params.username; 43 | 44 | const [gists_req, repos_req] = await Promise.all([ 45 | fetch(`https://api.github.com/users/${username}/gists`), 46 | fetch(`https://api.github.com/users/${username}/repos`), 47 | ]); 48 | 49 | const [gists, repos] = await Promise.all([ 50 | gists_req.json(), 51 | repos_req.json(), 52 | ]); 53 | 54 | return transform(gists, repos); 55 | } 56 | 57 | function transform(gists, repos) { 58 | const owner = gists.length ? gists[0].owner : repos[0].owner; 59 | const payload = { 60 | user: { 61 | username: owner.login, 62 | avatar: owner.avatar_url, 63 | url: owner.html_url, 64 | }, 65 | repos: [], 66 | gists: [], 67 | }; 68 | 69 | repos.sort((a, b) => { 70 | if (a.watchers_count > b.watchers_count) return -1; 71 | else if (a.watchers_count < b.watchers_count) return 1; 72 | }); 73 | 74 | gists.sort((a, b) => { 75 | if (a.comments > b.comments) return -1; 76 | else if (a.comments < b.comments) return 1; 77 | }); 78 | 79 | let repo_count = 0; 80 | for (const repo of repos) { 81 | if (repo.disabled || repo.archived || repo.private) continue; 82 | if (++repo_count > MAX_LIST) break; 83 | payload.repos.push({ 84 | name: repo.full_name, 85 | url: repo.html_url, 86 | desc: repo.description, 87 | created: repo.created_at, 88 | updated: repo.updated_at, 89 | watchers: repo.watchers_count, 90 | forks: repo.forks_count, 91 | }); 92 | } 93 | 94 | let gist_count = 0; 95 | for (const gist of gists) { 96 | if (!gist.public) continue; 97 | if (++gist_count > MAX_LIST) break; 98 | payload.gists.push({ 99 | url: gist.html_url, 100 | desc: gist.description, 101 | created: gist.created_at, 102 | updated: gist.updated_at, 103 | comments: gist.comments, 104 | }); 105 | } 106 | 107 | return JSON.stringify(payload, null, 4); 108 | } 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/Hello-World-App.md: -------------------------------------------------------------------------------- 1 | Here's an example of a Hello World application built using Osgood. This 2 | application will only use a single route, `GET /hello`, and so we'll only need 3 | two files. The [Application File](Osgood-Application-File) handles application 4 | routing, configuration, and policies. The [Worker File](Osgood-Worker-Files) 5 | handles the actual application logic for the route. 6 | 7 | ### Application File: `app.js` 8 | 9 | This configuration file is fairly minimal. For a larger overview check out the 10 | [Application File](Osgood-Application-File) page. 11 | 12 | Here we configure the app to listen on port `3000`. We also define a route at 13 | `GET /hello`, which will be routed to the worker file `hello.js`. Also, since 14 | the application doesn't need to perform any I/O, the policy configuration 15 | argument is just a noop function. This means the worker _cannot_ perform any 16 | I/O, even if an attacker were able to `eval()` arbitrary code within the 17 | worker. 18 | 19 | ```javascript 20 | app.port = 3000; 21 | app.get('/hello', 'hello.js', policy => {}); 22 | ``` 23 | 24 | ### Worker File `hello.js` 25 | 26 | Here we describe the application code. The default exported function accepts 27 | two arguments, `request` and `context`. The `request` argument is an instance 28 | of the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) 29 | class available in modern browsers. The second argument, `context`, contains 30 | some additional information described in the [Osgood Worker 31 | Files](Osgood-Worker-Files#requests) page. 32 | 33 | This function can either return a promise (either directly or by virtue of 34 | being an `async` function), or it can return a value directly. In this example 35 | we're simply returning a string which will then be sent to the client. 36 | 37 | ```javascript 38 | export default async (request, context) => { 39 | return "Hello, World!"; 40 | }; 41 | ``` 42 | 43 | ### Command Line 44 | 45 | Now let's execute our application. Once you've followed along with the 46 | [install](Installation) instructions you're then ready to execute the code. Run 47 | the following commands in two different terminals to call your application 48 | code: 49 | 50 | ```shell 51 | $ osgood ./app.js 52 | $ curl http://localhost:3000/hello 53 | ``` 54 | 55 | Once the `curl` call is complete you should see the text `Hello, World!` 56 | displayed in your terminal. 57 | -------------------------------------------------------------------------------- /docs/Home.md: -------------------------------------------------------------------------------- 1 | - [Osgood Overview](Osgood-Overview) 2 | - [Installation](Installation) 3 | 4 | ## API Reference 5 | 6 | - [Osgood Application File](Osgood-Application-File): One file which handles routing and security policies 7 | - [Osgood Worker Files](Osgood-Worker-Files): At least one file for handling application logic 8 | - [JavaScript Features](JavaScript-Features): Osgood supports a different set of features than web browsers 9 | - [Command Line Usage](Command-Line-Usage): Running Osgood from the CLI 10 | 11 | ## Sample Applications 12 | 13 | - [App #1: Hello World](Hello-World-App) 14 | - [App #2: Merge API Requests](GitHub-API-Merge-App) 15 | - [App #3: CouchDB CRUD](CouchDB-CRUD-App) 16 | 17 | ## Contributing 18 | 19 | - [Building / Compiling](Building) 20 | -------------------------------------------------------------------------------- /docs/Installation.md: -------------------------------------------------------------------------------- 1 | There are a few ways to install Osgood. 2 | 3 | ## Download a Prebuilt Release 4 | 5 | First, visit the [Releases](https://github.com/IntrinsicLabs/osgood/releases) 6 | page. There you will find different builds available for two of the two major 7 | platforms. From there you can download a binary and run it. 8 | 9 | - [Osgood for 10 | MacOS](https://github.com/IntrinsicLabs/osgood/releases/download/0.1.0/osgood-osx-0.1.0.zip) 11 | - [Osgood for 12 | Linux](https://github.com/IntrinsicLabs/osgood/releases/download/0.1.0/osgood-linux-0.1.0.zip) 13 | 14 | ## npm 15 | 16 | As a matter of convenience, you can also install Osgood using npm. This will 17 | put the `osgood` binary somewhere in your path automatically. You can do so by 18 | running the following: 19 | 20 | ```shell 21 | npm install -g osgood 22 | ``` 23 | 24 | ## Build from Scratch 25 | 26 | The third method is to [build](Building) the project and run the binary you 27 | compile locally. 28 | -------------------------------------------------------------------------------- /docs/JavaScript-Features.md: -------------------------------------------------------------------------------- 1 | Osgood runs your JavaScript code using the V8 engine and therefore provides 2 | fairly modern features. For example, the following are available: 3 | 4 | - `async` functions / `await` keyword 5 | - `class` syntax and private fields 6 | - `BigInt` 7 | - destructuring assignments 8 | - `WeakMap`, `WeakSet` 9 | - `const`, `let` declarations 10 | - arrow functions 11 | - default parameters 12 | - property shorthands 13 | - regular expression named capture groups 14 | 15 | JavaScript is interesting because there are many features which aren't 16 | technically part of the core language. Popular functions like `alert()`, 17 | `setTimeout()`, `atob()`, and `fetch()` are examples. They are instead features 18 | added by various implementations (such as browsers). 19 | 20 | We specifically chose to provide features beneficial to server side JavaScript 21 | applications—features that are based on existing implementations available in 22 | other JavaScript environments. 23 | 24 | We provide many globals which are commonly used in other JavaScript runtimes. 25 | Here's a list of them: 26 | 27 | ### Generic Features 28 | 29 | - `console.{log,error,warn,info,debug,trace}` 30 | - `setTimeout` / `setInterval` / `clearTimeout` / `clearInterval` 31 | - `atob` / `btoa` 32 | - `ReadableStream`, `WritableStream`, `TransformStream` 33 | - `TextEncoder`, `TextDecoder` 34 | - `URL`, `URLSearchParams` 35 | 36 | ### Fetch API 37 | 38 | Instead of building an interface for making requests from scratch we chose to 39 | implement the familiar `fetch()` interface provided by browsers. These classes 40 | and functions all revolve around the [Fetch 41 | API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). 42 | 43 | - `Request`: read more on [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) 44 | - `Response`: read more on [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) 45 | - `Headers`: read more on [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) 46 | - `fetch()`: read more on [fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) 47 | - `FormData`: read more on [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) 48 | -------------------------------------------------------------------------------- /docs/Osgood-Application-File.md: -------------------------------------------------------------------------------- 1 | An Osgood Application file has access to a global object called `app`. By 2 | setting various properties on this object, we're able to configure the behavior 3 | of our application. 4 | 5 | ## App Configuration 6 | 7 | `app.interface`: This is the name of the interface we'll listen on. It defaults 8 | to `0.0.0.0`, which means all interfaces. You can also set it to `127.0.0.1`, 9 | which means only requests from the local machine will work. You can also set it 10 | to the IP address of a hardware interface on your machine. 11 | 12 | `app.port`: This is the port which Osgood will listen on. By default, it listens 13 | on `8080`. 14 | 15 | ```javascript 16 | app.interface = '127.0.0.1'; 17 | app.port = 8080; 18 | app.host = 'localhost'; 19 | ``` 20 | 21 | ## Routing 22 | 23 | After the application basics have been configured, we can go ahead and configure 24 | the different routes used in our application. This can be done by calling 25 | methods on the `app` objects. Each of these methods have the same signature: 26 | 27 | - `app.get(routePattern, workerFilename, policyFunction)` 28 | - `app.post(...)` 29 | - `app.put(...)` 30 | - `app.patch(...)` 31 | - `app.delete(...)` 32 | - `app.head(...)` 33 | - `app.options(...)` 34 | - `app.trace(...)` 35 | - `app.connect(...)` 36 | 37 | ### Route Pattern 38 | 39 | The route pattern is essentially a 40 | [glob](https://www.npmjs.com/package/glob#glob-primer) with the added ability 41 | to extract URL parameters. It is specifically matched against the path of the 42 | requested URL. Here's a quick explanation of how it works: 43 | 44 | - An asterisk (`*`) refers to any character that isn't a forward slash 45 | - A colon (`:`) followed by `[a-zA-Z0-9_]` is similar to an asterisk but is captured for `context.params.paramName` 46 | - A double asterisk (`**`) refers to any character including a forward slash 47 | 48 | ### Worker Filename 49 | 50 | This is the path to the file to be loaded for handling requests. 51 | 52 | ### Policy Function 53 | 54 | Policy Functions are used for configuring the security policies used by Osgood 55 | Workers. They're configured by calling methods available on the `policy` 56 | argument. The only policies currently available in Osgood are available on the 57 | `policy.outboundHttp` object. 58 | 59 | The following example will _only_ match requests for `GET http://localhost:8000/users`: 60 | 61 | ```javascript 62 | app.get('/users', 'foo.js', policy => { 63 | policy.outboundHttp.allowGet('http://localhost:5984/users/_all_docs'); 64 | }); 65 | ``` 66 | 67 | ### Routing Examples 68 | 69 | The following example will match requests for `GET 70 | http://localhost:8000/users/admin` but will not match requests for either `GET 71 | http://localhost:8000/users/admin/xyz` or `POST 72 | http://localhost:8000/users/admin`: 73 | 74 | ```javascript 75 | app.get('/users/:id', 'view.js', policy => { 76 | policy.outboundHttp.allowGet('http://localhost:5984/users/*'); 77 | }); 78 | ``` 79 | 80 | ## API Security Policies 81 | 82 | Osgood applies the _Principle of Least Privilege_ on a per-worker basis. This 83 | means that by default a worker isn't allowed to talk to third party services. 84 | By writing policies the developer is able to whitelist ahead of time which 85 | outbound services can be communicated with, and how they may be communicated 86 | with. 87 | 88 | These policies are based on our existing [Intrinsic for Node.js HTTP 89 | Policies](https://intrinsic.com/docs/latest/policy-outbound-http.html), which 90 | have proved to be a simple and effective approach for securing servers. 91 | 92 | A policy function looks like this: 93 | 94 | ```javascript 95 | (policy) => { 96 | policy.outboundHttp.allowGet('http://api.local:123/users/*'); 97 | policy.outboundHttp.allowPost('http://api.local:123/widgets/**'); 98 | } 99 | ``` 100 | 101 | Policies are configured by using the `policy.outboundHttp` object. This object 102 | has several methods correlating to popular HTTP methods, each with the same 103 | signature: 104 | 105 | - `allowGet(urlPattern)` 106 | - `allowPost(...)` 107 | - `allowPut(...)` 108 | - `allowPatch(...)` 109 | - `allowDelete(...)` 110 | - `allowHead(...)` 111 | - `allowOptions(...)` 112 | - `allowTrace(...)` 113 | - `allowConnect(...)` 114 | 115 | The `urlPattern` argument is similar in syntax to the incoming HTTP request 116 | pattern, except that there are no parameter capturing. 117 | 118 | - An asterisk (`*`) refers to any character that isn't a forward slash 119 | - A double asterisk (`**`) refers to any character including a forward slash 120 | 121 | Port numbers should only be supplied if the URL is using using a port which 122 | doesn't match the protocol, for example `http:` with `80` or `https:` with 123 | `443`. 124 | 125 | ## Static Routes 126 | 127 | Static routes can be configured using the `app.static()` method. 128 | 129 | ```javascript 130 | app.static(routePrefix, path); 131 | ``` 132 | 133 | Unlike the other routes which accept complex patterns, the `routePrefix` 134 | argument here only works as a prefix. For example with a value of `/assets`, 135 | any request falling under `http://localhost:3000/assets` will trigger the 136 | static route. 137 | 138 | The `path` argument is a path to a directory to serve content from. With a 139 | value set to `public`, and a `routePrefix` set to `/assets`, a request for 140 | `http://localhost:3000/assets/styles/main.css` will translate to 141 | `./public/styles/main.css`. 142 | 143 | The `Content-Type` header is inferred solely based on the file extension. For 144 | example, the file `style.css` will result in `text/css`, whereas a file without 145 | an extension such as `foobar` will fallback to `application/octet-stream`. 146 | 147 | ### Caveats: 148 | 149 | - The `path` argument must point to a directory, not a file 150 | - There is no concept of an `index` file 151 | - Any request directly to a directory without a filename (i.e. `/assets`) will fail 152 | - We may allow this to be configurable in the future, e.g., setting to `index.html` 153 | -------------------------------------------------------------------------------- /docs/Osgood-Overview.md: -------------------------------------------------------------------------------- 1 | Osgood is a platform for running JavaScript on the server. It aims to be 2 | secure, fast, and simple. 3 | 4 | ## Secure 5 | 6 | Osgood applications are configured by specifying a list of route patterns and 7 | indicating which Osgood worker to invoke. Each Osgood worker has its own set of 8 | policies and are isolated from other workers. 9 | 10 | ## Fast 11 | 12 | Osgood typically runs faster than equivalent technologies. For example, on a 13 | benchmark where JavaScript returns `Hello, world!`, Osgood runs about 20% 14 | faster than Node.js. 15 | 16 | ## Simple 17 | 18 | Running an Osgood application is as simple as downloading a static [osgood 19 | binary](Installation) and passing in the path to an application file, and 20 | writing a second Osgood Worker file to handle the application logic. 21 | 22 | * * * 23 | 24 | Check out the [install](Installation) page if you'd like to install Osgood. 25 | -------------------------------------------------------------------------------- /docs/Osgood-Worker-Files.md: -------------------------------------------------------------------------------- 1 | Osgood Worker Files are given a different set of globals than the application 2 | file. For example, there is no `app` global available. 3 | 4 | Each worker file will run in a separate thread from the others, as well as a 5 | separate thread from the application file. This means that no global state can 6 | be shared between them. This also means that if two different workers `import` 7 | the same file, no instantiated singletons may be shared. 8 | 9 | ## Requests 10 | 11 | A worker file works by exporting a default function. This function will receive 12 | two arguments. An example looks like the following: 13 | 14 | ```javascript 15 | export default async (request, context) => { 16 | console.log(request.url); // 'http://localhost:8000/users/tlhunter' 17 | console.log(request.headers); // instanceof Headers 18 | console.log(request.method); // 'POST' 19 | console.log(request.body); // instanceof ReadableStream 20 | console.log(context.params); // { username: 'tlhunter' } 21 | console.log(context.query); // instanceof URLSearchParams 22 | } 23 | ``` 24 | 25 | The `request` argument is an instance of 26 | [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request). It 27 | contains only the most basic information about the incoming request. The 28 | `context` argument provides some additional niceties added by Osgood. 29 | 30 | ### Parsing an Incoming Body 31 | 32 | Parsing an incoming body works the same way as it would inside of a Service 33 | Worker in your browser. If you're receiving a JSON request from the client, 34 | such as within a POST request, you can have the content parsed for you by 35 | running the following: 36 | 37 | ```javascript 38 | const body = await request.json(); 39 | ``` 40 | 41 | Keep in mind that if the request contains invalid JSON, the operation will 42 | throw an error. 43 | 44 | 45 | ## Responses 46 | 47 | An Osgood Worker function decides what response to provide to the client based 48 | on the return value. If a promise is returned then the resolved value is used 49 | for the response. Otherwise, if a simple object or string is returned, then 50 | that will be used as the response. For the most control an Osgood Worker can 51 | return an instance of 52 | [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), which 53 | allows setting headers and a status code. 54 | 55 | However, there are a few caveats to this approach that you should be aware of. 56 | 57 | ### Default Values 58 | 59 | Osgood will attempt to provide a default `Content-Type` header when a value is 60 | returned which isn't an instance of Response. 61 | 62 | #### String 63 | 64 | If a string is returned, then the content type will be set to `text/plain`. If 65 | you plan on returning a different value, such as HTML, you'll need to make use 66 | of a Response object and set the headers manually. If you want to return 67 | another primitive value, like a `boolean` or a `number`, then you'll need to 68 | manually convert it into a string first. 69 | 70 | #### TypedArray or ArrayBuffer 71 | 72 | If an instance of a `TypedArray`—such as `Uint8Array`—or an `ArrayBuffer` is 73 | returned then the content type will be set to `application/octet-stream`. 74 | 75 | #### POJO Object 76 | 77 | A POJO (Plain Ol' JavaScript Object) is an object with a prototype set to 78 | either `null` or to `Object.prototype`. Specifically, it is a simple object 79 | probably created manually with `{}` brackets, and is not an instance of a 80 | class. 81 | 82 | If a value being returned is a POJO, then the value will be converted into a 83 | JSON string representation and the content type header will be set to 84 | `application/json`. This is convenient for spinning up simple API servers. 85 | 86 | #### Class Instance 87 | 88 | However, if the value is an Object but not a POJO, such as an instance of a 89 | class, then we won't simply convert the object into JSON and reply with it. 90 | This may sound like a pain but it was actually a deliberate decision chosen for 91 | security reasons. 92 | 93 | Consider, for example, a `User` class which has a `username` and `displayName` 94 | property. This seems like a likely object to serialize into a string. However, 95 | if deep within the application someone modifies the object to then contain a 96 | `password` field, the application is now accidentally leaking private data. 97 | 98 | ```javascript 99 | // Anti-Pattern: This will fail 100 | class User { } 101 | const joe = new User(); 102 | export default function() { 103 | return joe; 104 | } 105 | ``` 106 | 107 | For this reason, if a class instance is returned, or an object which at any 108 | point contains a class instance, the request will fail. 109 | 110 | The pattern we would like to promote is specifically returning a new POJO 111 | object at the end of a worker. This is convenient because the "contract" of 112 | your application is clearly defined, and deeper changes within an application 113 | don't affect output (which can potentially break consuming code): 114 | 115 | ```javascript 116 | export default function() { 117 | const joe = new User(); 118 | 119 | return { 120 | username: joe.username, 121 | displayName: joe.displayname 122 | }; 123 | } 124 | ``` 125 | 126 | #### Class Instance with `.toJSON()` 127 | 128 | As part of our decision to deliberately prevent class instances from being 129 | passed as responses, we did specifically make it acceptable to provide class 130 | instances with a `.toJSON()` method. We chose this because the developer is 131 | then intentionally specifying exactly which properties should be returned in 132 | the response. 133 | 134 | ```javascript 135 | // This is OK 136 | class User { 137 | constructor(username, password) { 138 | this.username = username; 139 | this.password = password; 140 | } 141 | toJSON() { 142 | return { 143 | username: this.username 144 | }; 145 | } 146 | } 147 | const joe = new User('joe', 'hunter12'); 148 | export default function() { 149 | return joe; 150 | } 151 | ``` 152 | 153 | The same rules apply for deeply nested class instances so make sure any object 154 | you return are either not class instances or contain a `toJSON` method. 155 | 156 | ### Response Object 157 | 158 | For more control over the response one can return an instance of the `Response` 159 | object. This allows for setting things like headers and status codes (which are 160 | otherwise set to `200`). Here's an example of how to do this: 161 | 162 | ```javascript 163 | export default function(request, context) { 164 | const payload = { 165 | isCool: context.params.username 166 | }; 167 | const status = 451; 168 | 169 | const headers = new Headers({ 170 | 'Content-Type': 'application/vnd.widgetcorp+json' 171 | }); 172 | 173 | const body = JSON.stringify(payload); 174 | 175 | const response = new Response(body, { 176 | headers, 177 | status 178 | }); 179 | 180 | return response; 181 | }; 182 | ``` 183 | -------------------------------------------------------------------------------- /examples/contact/app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osgood 2 | 3 | app.port = 3000; 4 | 5 | app.static('/', './static'); 6 | 7 | app.route('POST', '/contact', 'contact.js', policy => { 8 | policy.outboundHttp.allowPost('https://api.mailgun.net/v3/samples.mailgun.org/messages'); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/contact/contact.js: -------------------------------------------------------------------------------- 1 | export default async (request, context) => { 2 | try { 3 | var incoming = await request.json(); 4 | } catch (e) { 5 | return json({error: 'CANNOT_PARSE', message: "Invalid JSON Payload Provided"}, 400); 6 | } 7 | 8 | const email = new FormData(); 9 | 10 | if (!('email' in incoming) || !('name' in incoming) || !('message' in incoming)) { 11 | return json({error: 'MISSING_FIELDS', message: "Invalid JSON Payload Provided"}, 400); 12 | } else if (typeof incoming.email !== 'string' || typeof incoming.name !== 'string' || typeof incoming.message !== 'string') { 13 | return json({error: 'INVALID_FIELD_TYPES', message: "Invalid JSON Payload Provided"}, 400); 14 | } else if (!incoming.name) { 15 | return json({error: 'EMPTY_VALUE', message: "You forgot to supply a name!", field: 'name'}, 400); 16 | } else if (!incoming.email) { 17 | return json({error: 'EMPTY_VALUE', message: "You forgot to supply an email address!", field: 'email'}, 400); 18 | } else if (!incoming.message) { 19 | return json({error: 'EMPTY_VALUE', message: "You forgot to supply a message!", field: 'message'}, 400); 20 | } 21 | 22 | email.append('from', `${incoming.name} <${incoming.email}>`); 23 | email.append('to', 'spam@intrinsic.com'); 24 | email.append('subject', "Contact form email"); 25 | email.append('text', incoming.message); 26 | 27 | // URL and API Key are samples from the Mailgun docs 28 | // This URL is _only_ a demo for parsing multipart/form-data fields 29 | // This will _not_ send a real email 30 | try { 31 | await fetch('https://api.mailgun.net/v3/samples.mailgun.org/messages', { 32 | method: 'POST', 33 | headers: new Headers({ 34 | Authorization: 'Basic ' + btoa('api:key-3ax6xnjp29jd6fds4gc373sgvjxteol0') 35 | }), 36 | body: email 37 | }); 38 | } catch (e) { 39 | return json({error: 'CANNOT_SEND', message: "Cannot parse provided JSON"}, 500); 40 | } 41 | 42 | return json({success: true, message: "Email has been sent"}); 43 | } 44 | 45 | function json(obj, status = 200) { 46 | const headers = new Headers({ 47 | 'Content-Type': 'application/json' 48 | }); 49 | 50 | const body = JSON.stringify(obj); 51 | 52 | const response = new Response(body, { headers, status }); 53 | 54 | return response; 55 | } 56 | -------------------------------------------------------------------------------- /examples/contact/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My Little Website 5 | 6 | 7 | 8 |

My Little Website

9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/contact/static/scripts/contact.js: -------------------------------------------------------------------------------- 1 | const $name = document.getElementById('name'); 2 | const $email = document.getElementById('email'); 3 | const $message = document.getElementById('message'); 4 | const $form = document.getElementById('form'); 5 | const $status = document.getElementById('status'); 6 | 7 | $form.addEventListener('submit', (event) => { 8 | event.preventDefault(); 9 | 10 | (async () => { 11 | const payload = { 12 | name: $name.value, 13 | email: $email.value, 14 | message: $message.value, 15 | }; 16 | 17 | try { 18 | var response = await fetch('/contact', { 19 | method: 'POST', 20 | headers: { 21 | 'Content-Type': 'application/json' 22 | }, 23 | body: JSON.stringify(payload) 24 | }); 25 | } catch(e) { 26 | $status.innerHTML = `Failed to send: ${e.message}`; 27 | return; 28 | } 29 | 30 | const body = await response.json(); 31 | 32 | if (body.error) { 33 | $status.innerHTML = `Failed to send: ${body.message}`; 34 | return; 35 | } 36 | $status.innerHTML = `success: ${body.message}`; 37 | 38 | })(); 39 | 40 | return false; 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /examples/contact/static/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #222; 3 | color: #ddd; 4 | font-family: monospace; 5 | } 6 | .success { 7 | color: #22dd22; 8 | } 9 | .fail { 10 | color: #dd2222; 11 | } 12 | -------------------------------------------------------------------------------- /examples/couchdb-rest/README.md: -------------------------------------------------------------------------------- 1 | # setup 2 | 3 | Run CouchDB: 4 | 5 | ```sh 6 | docker run \ 7 | -e COUCHDB_USER=osgood_admin \ 8 | -e COUCHDB_PASSWORD=hunter12 \ 9 | -p 5984:5984 \ 10 | --name osgood-couch \ 11 | -d couchdb 12 | ``` 13 | 14 | Create a database: 15 | 16 | ```sh 17 | curl \ 18 | -X PUT \ 19 | http://osgood_admin:hunter12@localhost:5984/users 20 | ``` 21 | 22 | Generate POST request to create a document: 23 | 24 | ```sh 25 | curl \ 26 | -X POST \ 27 | http://localhost:8000/users \ 28 | -d '{"foo": "bar"}' \ 29 | -H "Content-Type: application/json" 30 | ``` 31 | 32 | Or, use the `commands.sh` script to interact with the REST API. This file 33 | requires that both `curl` and [`jq`](https://stedolan.github.io/jq/) are 34 | installed. Both are highly useful CLI tools. 35 | -------------------------------------------------------------------------------- /examples/couchdb-rest/app.js: -------------------------------------------------------------------------------- 1 | app.interface = '127.0.0.1'; 2 | app.port = 8000; 3 | app.host = 'localhost'; 4 | 5 | // TODO: Need an optional trailing slash 6 | app.get('/users', 'list.js', policy => { 7 | policy.outboundHttp.allowGet('http://localhost:5984/users/_all_docs'); 8 | }); 9 | 10 | app.get('/users/:id', 'view.js', policy => { 11 | policy.outboundHttp.allowGet('http://localhost:5984/users/*'); 12 | }); 13 | 14 | app.delete('/users/:id', 'delete.js', policy => { 15 | policy.outboundHttp.allowGet('http://localhost:5984/users/*'); 16 | policy.outboundHttp.allowDelete('http://localhost:5984/users/*'); 17 | }); 18 | 19 | // TODO: Need an optional trailing slash 20 | app.post('/users', 'create.js', policy => { 21 | policy.outboundHttp.allowPost('http://localhost:5984/users/'); 22 | }); 23 | 24 | app.put('/users/:id', 'update.js', policy => { 25 | policy.outboundHttp.allowGet('http://localhost:5984/users/*'); 26 | policy.outboundHttp.allowPut('http://localhost:5984/users/*'); 27 | }); 28 | -------------------------------------------------------------------------------- /examples/couchdb-rest/commands.sh: -------------------------------------------------------------------------------- 1 | # require `jq`: https://stedolan.github.io/jq/ 2 | 3 | echo "\ncreate user" 4 | curl -s -XPOST http://localhost:8000/users --data '{"username": "osgood"}' -H 'Content-Type: application/json' | jq "." 5 | 6 | echo "\nlist users" 7 | curl -s http://localhost:8000/users | jq "." 8 | 9 | echo "\nget user" 10 | curl -s http://localhost:8000/users/`curl -s http://localhost:8000/users | jq -r ".[0]"` | jq "." 11 | 12 | echo "\nupdate user" 13 | curl -s -XPUT http://localhost:8000/users/`curl -s http://localhost:8000/users | jq -r ".[0]"` -d '{"username": "osgood", "is_cool": true}' -H 'Content-Type: application/json' | jq "." 14 | 15 | echo "\ndelete user" 16 | curl -s -XDELETE http://localhost:8000/users/`curl -s http://localhost:8000/users | jq -r ".[0]"` | jq "." 17 | -------------------------------------------------------------------------------- /examples/couchdb-rest/common.js: -------------------------------------------------------------------------------- 1 | const AUTH = `Basic ${btoa('osgood_admin:hunter12')}`; 2 | 3 | export function json(obj, status = 200) { 4 | const headers = new Headers({ 5 | 'Content-Type': 'application/json' 6 | }); 7 | 8 | const body = JSON.stringify(obj); 9 | 10 | const response = new Response(body, { headers, status }); 11 | 12 | return response; 13 | } 14 | 15 | // Makes a request to CouchDB 16 | export function dbRequest(method = 'GET', path = '', body = '') { 17 | const options = { 18 | method, 19 | headers: { 20 | Authorization: AUTH 21 | } 22 | } 23 | 24 | if (body) { 25 | options.headers['Content-Type'] = 'application/json'; 26 | options.body = JSON.stringify(body); 27 | } 28 | 29 | return fetch(`http://localhost:5984/users/${path}`, options); 30 | } 31 | -------------------------------------------------------------------------------- /examples/couchdb-rest/create.js: -------------------------------------------------------------------------------- 1 | import { dbRequest, json } from './common.js'; 2 | 3 | export default async function main(request, context) { 4 | try { 5 | var record = await request.json(); 6 | } catch (e) { 7 | return json({error: 'CANNOT_PARSE'}, 401); 8 | } 9 | 10 | if (record.id || record._id) { 11 | return json({error: 'CANNOT_SPECIFY_ID'}, 401); 12 | } 13 | 14 | if (record.created || record.updated) { 15 | return json({error: 'CANNOT_CHANGE_DATES'}, 401); 16 | } 17 | 18 | record.created = (new Date()).toISOString(); 19 | record.updated = null; 20 | 21 | const payload = await dbRequest('POST', '', record); 22 | 23 | const obj = await payload.json(); 24 | 25 | if (obj.error) { 26 | return json({ error: 'UNABLE_TO_CREATE' }, 500); 27 | } 28 | 29 | record.id = obj.id; 30 | 31 | return json(record); 32 | } 33 | -------------------------------------------------------------------------------- /examples/couchdb-rest/delete.js: -------------------------------------------------------------------------------- 1 | import { dbRequest, json } from './common.js'; 2 | 3 | export default async function main(request, context) { 4 | const id = context.params.id; 5 | 6 | if (!id) { 7 | return json({ error: 'INVALID_REQUEST' }, 400); 8 | } 9 | 10 | const payload = await dbRequest('GET', id); 11 | 12 | const obj = await payload.json(); 13 | 14 | if (obj.error && obj.error === 'not_found') { 15 | return json({ error: 'NOT_FOUND' }, 404); 16 | } 17 | 18 | if (obj.error) { 19 | return json({ error: 'UNABLE_TO_DELETE' }, 500); 20 | } 21 | 22 | // WARNING: This isn't atomic 23 | 24 | const rev = obj._rev; 25 | 26 | const delete_payload = await dbRequest('DELETE', `${id}?rev=${rev}`); 27 | 28 | const delete_obj = await delete_payload.json(); 29 | 30 | delete obj._rev; // hide implementation detail 31 | obj.id = obj._id; 32 | delete obj._id; // hide implementation detail 33 | 34 | if (!delete_obj.ok) { 35 | return json({error: 'UNABLE_TO_DELETE'}, 500); 36 | } 37 | 38 | return json(obj); 39 | } 40 | -------------------------------------------------------------------------------- /examples/couchdb-rest/list.js: -------------------------------------------------------------------------------- 1 | import { dbRequest, json } from './common.js'; 2 | 3 | export default async function main(request, context) { 4 | const payload = await dbRequest('GET', '_all_docs'); 5 | 6 | const obj = await payload.json(); 7 | 8 | if (obj.error) { 9 | return json({ error: 'UNABLE_TO_LIST' }, 500); 10 | } 11 | 12 | const result = []; 13 | 14 | for (let row of obj.rows) { 15 | result.push(row.id); 16 | } 17 | 18 | // TODO: should be array of hydrated objects 19 | return json(result); 20 | } 21 | -------------------------------------------------------------------------------- /examples/couchdb-rest/update.js: -------------------------------------------------------------------------------- 1 | import { dbRequest, json } from './common.js'; 2 | 3 | export default async function main(request, context) { 4 | const id = context.params.id; 5 | 6 | if (!id) { 7 | return json({ error: 'INVALID_REQUEST' }, 400); 8 | } 9 | 10 | try { 11 | var record = await request.json(); 12 | } catch (e) { 13 | return json({error: 'CANNOT_PARSE'}, 401); 14 | } 15 | 16 | if ((record.id && record.id !== id) || (record._id && record._id !== id)) { 17 | return json({error: 'CANNOT_RENAME'}, 401); 18 | } 19 | 20 | if (record.created || record.updated) { 21 | return json({error: 'CANNOT_CHANGE_DATES'}, 401); 22 | } 23 | 24 | const existing_record = await dbRequest('GET', id); 25 | 26 | const existing_obj = await existing_record.json(); 27 | 28 | if (existing_obj.error && existing_obj.error === 'not_found') { 29 | return json({ error: 'NOT_FOUND' }, 404); 30 | } 31 | 32 | // WARNING: This isn't atomic 33 | 34 | const rev = existing_obj._rev; 35 | 36 | record._rev = rev; 37 | 38 | // retain existing created time 39 | record.created = existing_obj.created; 40 | record.updated = (new Date()).toISOString(); 41 | 42 | const update_payload = await dbRequest('PUT', id, record); 43 | 44 | const update_obj = await update_payload.json(); 45 | 46 | if (update_obj.error) { 47 | return json({ error: 'UNABLE_TO_UPDATE' }, 500); 48 | } 49 | 50 | delete record._rev; // hide implementation detail 51 | record.id = update_obj.id; 52 | 53 | return json(record); 54 | } 55 | -------------------------------------------------------------------------------- /examples/couchdb-rest/view.js: -------------------------------------------------------------------------------- 1 | import { dbRequest, json } from './common.js'; 2 | 3 | export default async function main(request, context) { 4 | const id = context.params.id; 5 | 6 | if (!id) { 7 | return json({ error: 'INVALID_REQUEST' }, 400); 8 | } 9 | 10 | const payload = await dbRequest('GET', id); 11 | 12 | const obj = await payload.json(); 13 | 14 | if (obj.error && obj.error === 'not_found') { 15 | return json({ error: 'NOT_FOUND' }, 404); 16 | } 17 | 18 | if (obj.error) { 19 | return json({ error: 'UNABLE_TO_VIEW' }, 500); 20 | } 21 | 22 | delete obj._rev; // hide implementation detail 23 | obj.id = obj._id; 24 | delete obj._id; // hide implementation detail 25 | 26 | return json(obj); 27 | } 28 | -------------------------------------------------------------------------------- /examples/simple/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM osgood 2 | 3 | COPY ./*.js /srv/osgood/ 4 | 5 | EXPOSE 8080 6 | 7 | CMD ["app.js"] 8 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple App 2 | 3 | This is a very simple application to get you started. To run it, do the 4 | following: 5 | 6 | ```sh 7 | /path/to/osgood app.js 8 | ``` 9 | 10 | Then you can make requests to the server and receive responses: 11 | 12 | ```sh 13 | curl http://localhost:8080/hello 14 | curl http://localhost:8080/gh-merge/IntrinsicLabs 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/simple/app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osgood 2 | 3 | app.get('/hello', 'hello.js'); 4 | 5 | app.route('GET', '/gh-merge/:username', 'gh-merge.js', policy => { 6 | policy.outboundHttp.allowGet('https://api.github.com/users/*/gists'); 7 | policy.outboundHttp.allowGet('https://api.github.com/users/*/repos'); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/simple/gh-merge.js: -------------------------------------------------------------------------------- 1 | const MAX_LIST = 3; 2 | 3 | console.log('curl http://localhost:8080/gh-merge/{USERNAME}'); 4 | 5 | export default async function main(request, context) { 6 | const username = context.params.username; 7 | 8 | const [gists_req, repos_req] = await Promise.all([ 9 | fetch(`https://api.github.com/users/${username}/gists`), 10 | fetch(`https://api.github.com/users/${username}/repos`), 11 | ]); 12 | 13 | // TODO: currently osgood doesn't provide the `json` method 14 | const [gists, repos] = await Promise.all([ 15 | gists_req.json(), 16 | repos_req.json(), 17 | ]); 18 | 19 | return transform(gists, repos); 20 | } 21 | 22 | function transform(gists, repos) { 23 | const owner = gists.length ? gists[0].owner : repos[0].owner; 24 | const payload = { 25 | user: { 26 | username: owner.login, 27 | avatar: owner.avatar_url, 28 | url: owner.html_url, 29 | }, 30 | repos: [], 31 | gists: [], 32 | }; 33 | 34 | repos.sort((a, b) => { 35 | if (a.watchers_count > b.watchers_count) return -1; 36 | else if (a.watchers_count < b.watchers_count) return 1; 37 | }); 38 | 39 | gists.sort((a, b) => { 40 | if (a.comments > b.comments) return -1; 41 | else if (a.comments < b.comments) return 1; 42 | }); 43 | 44 | let repo_count = 0; 45 | for (const repo of repos) { 46 | if (repo.disabled || repo.archived || repo.private) continue; 47 | if (++repo_count > MAX_LIST) break; 48 | payload.repos.push({ 49 | name: repo.full_name, 50 | url: repo.html_url, 51 | desc: repo.description, 52 | created: repo.created_at, 53 | updated: repo.updated_at, 54 | watchers: repo.watchers_count, 55 | forks: repo.forks_count, 56 | }); 57 | } 58 | 59 | let gist_count = 0; 60 | for (const gist of gists) { 61 | if (!gist.public) continue; 62 | if (++gist_count > MAX_LIST) break; 63 | payload.gists.push({ 64 | url: gist.html_url, 65 | desc: gist.description, 66 | created: gist.created_at, 67 | updated: gist.updated_at, 68 | comments: gist.comments, 69 | }); 70 | } 71 | 72 | return JSON.stringify(payload, null, 4); 73 | } 74 | 75 | -------------------------------------------------------------------------------- /examples/simple/hello.js: -------------------------------------------------------------------------------- 1 | console.log('curl http://localhost:8080/hello'); 2 | 3 | export default (request, context) => { 4 | console.log('REQUEST', request); 5 | console.log('CONTEXT', context); 6 | 7 | return "Hello, World!"; 8 | }; 9 | -------------------------------------------------------------------------------- /js/bootstrap/base64.js: -------------------------------------------------------------------------------- 1 | // adapted from: https://stackoverflow.com/a/23190164 2 | const tableStr = 3 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 4 | const table = tableStr.split(''); 5 | 6 | // TODO: We should consider throwing a InvalidCharacterError/DOMException 7 | // This would require creating a global.DOMException property for `instanceof` 8 | // https://html.spec.whatwg.org/multipage/webappapis.html#atob 9 | export function atob(base64) { 10 | if (/(=[^=]+|={3,})$/.test(base64)) { 11 | throw new TypeError('String contains an invalid character'); 12 | } 13 | 14 | base64 = base64.replace(/=/g, ''); 15 | 16 | const n = base64.length & 3; 17 | 18 | if (n === 1) { 19 | throw new Error('String contains an invalid character'); 20 | } 21 | 22 | for (var i = 0, j = 0, len = base64.length / 4, bin = []; i < len; ++i) { 23 | const a = tableStr.indexOf(base64[j++] || 'A'); 24 | const b = tableStr.indexOf(base64[j++] || 'A'); 25 | const c = tableStr.indexOf(base64[j++] || 'A'); 26 | const d = tableStr.indexOf(base64[j++] || 'A'); 27 | 28 | if ((a | b | c | d) < 0) { 29 | throw new TypeError('String contains an invalid character'); 30 | } 31 | 32 | bin[bin.length] = ((a << 2) | (b >> 4)) & 255; 33 | bin[bin.length] = ((b << 4) | (c >> 2)) & 255; 34 | bin[bin.length] = ((c << 6) | d) & 255; 35 | } 36 | 37 | return String.fromCharCode.apply(null, bin).substr(0, bin.length + n - 4); 38 | } 39 | 40 | export function btoa(bin) { 41 | const base64 = []; 42 | for (let i = 0, j = 0, len = bin.length / 3; i < len; ++i) { 43 | const a = bin.charCodeAt(j++), 44 | b = bin.charCodeAt(j++), 45 | c = bin.charCodeAt(j++); 46 | if ((a | b | c) > 255) { 47 | throw new TypeError('String contains an invalid character'); 48 | } 49 | 50 | base64[base64.length] = 51 | table[a >> 2] + 52 | table[((a << 4) & 63) | (b >> 4)] + 53 | (isNaN(b) ? '=' : table[((b << 2) & 63) | (c >> 6)]) + 54 | (isNaN(b + c) ? '=' : table[c & 63]); 55 | } 56 | 57 | return base64.join(''); 58 | } 59 | -------------------------------------------------------------------------------- /js/bootstrap/body_mixin.js: -------------------------------------------------------------------------------- 1 | import { StringReadable, isBufferish } from 'internal:common.js'; 2 | import Headers from 'internal:headers.js'; 3 | 4 | const { getPrivate } = _bindings; 5 | 6 | const rawHeadersSym = getPrivate('rawHeaders'); 7 | const headersSym = getPrivate('headers'); 8 | const bodySym = getPrivate('body'); 9 | const _bodyStringSym = getPrivate('_bodyString'); 10 | const chunksSym = getPrivate('chunks'); 11 | export const writeChunkSym = getPrivate('writeChunk'); 12 | const writerSym = getPrivate('writer'); 13 | 14 | export class BodyMixin { 15 | // #rawHeaders; // not yet instantiated 16 | // #headers; // instantiated 17 | // #body; 18 | // #_bodyString; 19 | 20 | static init(body, initObj = {}) { 21 | if (!(initObj.headers instanceof Headers)) { 22 | this[rawHeadersSym] = initObj.headers; 23 | } else { 24 | this[headersSym] = initObj.headers; 25 | } 26 | 27 | if (body === writeChunkSym) { 28 | this[chunksSym] = []; 29 | } else if (body instanceof ReadableStream || body instanceof TransformStream || body instanceof FormData) { 30 | this[bodySym] = body; 31 | } else if (typeof body === 'string') { 32 | this[_bodyStringSym] = body; 33 | } else if (isBufferish(body)) { 34 | this[bodySym] = new StringReadable(body); 35 | } 36 | } 37 | 38 | get headers() { 39 | if (!this[headersSym]) { 40 | this[headersSym] = new Headers(this[rawHeadersSym]); 41 | } 42 | return this[headersSym]; 43 | } 44 | 45 | get body() { 46 | if (!this[bodySym] && this[chunksSym]) { 47 | let writer; 48 | const stream = new ReadableStream({ 49 | start(controller) { 50 | writer = controller; 51 | } 52 | }); 53 | this[writerSym] = writer; 54 | for (const chunk of this[chunksSym]) { 55 | if (typeof chunk === 'undefined') { 56 | writer.close(); 57 | } else { 58 | writer.enqueue(chunk); 59 | } 60 | } 61 | delete this[chunksSym]; 62 | this[bodySym] = stream; 63 | } 64 | return this[bodySym]; 65 | } 66 | 67 | get _bodyString() { 68 | return this[_bodyStringSym]; 69 | } 70 | 71 | async arrayBuffer() { 72 | let bufs = []; 73 | const lengths = []; 74 | let totalLength = 0; 75 | for await (let buf of this.body) { 76 | if (typeof buf === 'string') { 77 | const encoder = new TextEncoder(); 78 | buf = encoder.encode(buf).buffer; 79 | } 80 | bufs.push(buf); 81 | const len = buf.byteLength; 82 | lengths.push(len); 83 | totalLength += len; 84 | } 85 | const result = new Uint8Array(totalLength); 86 | let idx = 0; 87 | for (const [i, buf] of Object.entries(bufs)) { 88 | result.set(new Uint8Array(buf), idx); 89 | idx += lengths[i]; 90 | } 91 | return result.buffer; 92 | } 93 | 94 | async text() { 95 | let result = ''; 96 | for await (let chunk of this.body) { 97 | if (typeof chunk === 'object' && chunk !== null && isBufferish(chunk)) { 98 | const decoder = new TextDecoder(); 99 | chunk = decoder.decode(chunk); 100 | } 101 | if (typeof chunk === 'string') { 102 | result += chunk; 103 | } else { 104 | result += String(chunk); 105 | } 106 | } 107 | return result; 108 | } 109 | 110 | async json() { 111 | return JSON.parse(await this.text()); 112 | } 113 | 114 | static mixin(klass) { 115 | const descs = Object.getOwnPropertyDescriptors(BodyMixin.prototype); 116 | for (let [key, desc] of Object.entries(descs)) { 117 | if (key === 'constructor') { 118 | continue; 119 | } 120 | Object.defineProperty(klass.prototype, key, desc); 121 | } 122 | } 123 | } 124 | 125 | 126 | export function writeChunk(chunk) { 127 | if (this[chunksSym]) { 128 | this[chunksSym].push(chunk); 129 | } else { 130 | const writer = this[writerSym]; 131 | if (typeof chunk === 'undefined') { 132 | writer.close(); 133 | } else { 134 | writer.enqueue(chunk); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /js/bootstrap/common.js: -------------------------------------------------------------------------------- 1 | export function isBufferish(chunk) { 2 | if (!chunk) { 3 | return false; 4 | } 5 | return chunk instanceof ArrayBuffer || 6 | (chunk.buffer && chunk.buffer instanceof ArrayBuffer); 7 | } 8 | 9 | export function unimplemented() { 10 | throw new Error('Unimplemented!'); 11 | } 12 | 13 | // FIXME: This currently only implements pair iterators! 14 | // https://heycam.github.io/webidl/#idl-iterable 15 | export class IteratorMixin { 16 | // https://heycam.github.io/webidl/#es-iterable-entries 17 | entries() { 18 | return this[Symbol.iterator](); 19 | } 20 | 21 | // https://heycam.github.io/webidl/#es-forEach 22 | forEach() { 23 | unimplemented(); 24 | } 25 | 26 | // https://heycam.github.io/webidl/#es-iterable-keys 27 | *keys() { 28 | for (const [key, value] of this) { 29 | yield key; 30 | } 31 | } 32 | 33 | // https://heycam.github.io/webidl/#es-iterable-values 34 | *values() { 35 | for (const [key, value] of this) { 36 | yield value; 37 | } 38 | } 39 | 40 | static mixin(klass) { 41 | if (!(Symbol.iterator in klass.prototype)) { 42 | throw new Error('Cannot mixin IteratorMixin because class is not iterable'); 43 | } 44 | for (const key of Reflect.ownKeys(IteratorMixin.prototype)) { 45 | if (key === 'constructor') { 46 | continue; 47 | } 48 | if (key in klass.prototype) { 49 | throw new Error(`Cannot mixin IteratorMixin because key '${key}' already exists`); 50 | } 51 | klass.prototype[key] = IteratorMixin.prototype[key]; 52 | } 53 | } 54 | } 55 | 56 | export class StringReadable extends ReadableStream { 57 | constructor(string) { 58 | super({ 59 | start(controller) { 60 | controller.enqueue(string); 61 | controller.close(); 62 | } 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /js/bootstrap/console.js: -------------------------------------------------------------------------------- 1 | import FormData from 'internal:form_data.js'; 2 | 3 | const { 4 | _log, 5 | _error, 6 | } = self._bindings; 7 | 8 | function inspect(obj) { 9 | const INDENT = 2; 10 | const seen = new WeakSet(); 11 | let depth = 0; 12 | 13 | function pad(groupTerm) { 14 | return ' '.repeat(groupTerm ? depth - INDENT : depth); 15 | } 16 | 17 | function dive(node) { 18 | depth += INDENT; 19 | if (node === null) { 20 | depth -= INDENT; 21 | return `null`; 22 | } 23 | if (Array.isArray(node)) { 24 | let result = `[\n`; 25 | for (let item of node) { 26 | result += `${pad()}${dive(item)}\n`; 27 | } 28 | result += `${pad(true)}]`; 29 | depth -= INDENT; 30 | return result; 31 | } 32 | if (node instanceof URLSearchParams) { 33 | depth -= INDENT; 34 | return `URLSearchParams { ${node.toString()} }`; 35 | } 36 | if (node instanceof FormData) { 37 | depth -= INDENT; 38 | const pairs = []; 39 | for (const fd of node) { 40 | // Using stringify to escape quotes and remove ambiguity for human reader 41 | pairs.push(`${fd[0]}=${JSON.stringify(fd[1])}`); 42 | } 43 | return `FormData { ${pairs.join(', ')} }`; 44 | } 45 | 46 | const type = typeof node; 47 | 48 | switch (type) { 49 | case 'undefined': 50 | depth -= INDENT; 51 | return `undefined`; 52 | case 'function': 53 | depth -= INDENT; 54 | return `${node.name}(${node.length})`; 55 | case 'bigint': 56 | depth -= INDENT; 57 | return `${node}n`; 58 | case 'number': 59 | case 'boolean': 60 | case 'symbol': 61 | depth -= INDENT; 62 | return `${String(node)}`; 63 | case 'string': 64 | depth -= INDENT; 65 | return `'${node}'`; 66 | case 'object': 67 | if (seen.has(node)) { 68 | depth -= INDENT; 69 | return `[CIRCULAR]`; 70 | } 71 | seen.add(node); 72 | const keys = Reflect.ownKeys(node); 73 | let result = `${ 74 | node.constructor !== Object && node.constructor !== undefined ? node.constructor.name + ' ' : '' 75 | }{\n`; 76 | for (let key of keys) { 77 | result += `${pad()}${String(key)}: ${dive(node[key])}\n`; 78 | } 79 | result += `${pad(true)}}`; 80 | depth -= INDENT; 81 | return result; 82 | default: 83 | throw new Error(`unknown type: ${type}`); 84 | } 85 | } 86 | 87 | return dive(obj); 88 | } 89 | 90 | const formatLog = args => 91 | args.map(x => (typeof x === 'string' ? x : inspect(x))).join(' '); 92 | 93 | console.log = (...args) => { 94 | _log(formatLog(args)); 95 | }; 96 | 97 | console.error = (...args) => { 98 | _error(formatLog(args)); 99 | }; 100 | 101 | console.warn = (...args) => { 102 | _error(formatLog(args)); 103 | }; 104 | 105 | console.info = (...args) => { 106 | _log(formatLog(args)); 107 | }; 108 | 109 | console.debug = (...args) => { 110 | _log(formatLog(args)); 111 | }; 112 | 113 | console.trace = (...args) => { 114 | const { stack } = new Error(); 115 | const formattedStack = stack 116 | .split('\n') 117 | .slice(2) 118 | .join('\n'); 119 | _log(`${formatLog(args)}\n${formattedStack}`); 120 | }; 121 | -------------------------------------------------------------------------------- /js/bootstrap/context.js: -------------------------------------------------------------------------------- 1 | const { _route, getPrivate } = self._bindings; 2 | const urlSym = getPrivate('url'); 3 | const querySym = getPrivate('query'); 4 | const paramsSym = getPrivate('params'); 5 | 6 | const REGEX_CAPTURE_GROUPS = /\:([a-zA-Z0-9_]+)/g; 7 | const REPLACE_CAPTURE_GROUPS = '(?<$1>[^\\/]+)'; // named capture groups 8 | 9 | const REGEX_DOUBLE_ASTERISK = /\*\*/g; 10 | const REPLACE_DOUBLE_ASTERISK = '(.+)'; // unnamed capture group 11 | 12 | const REGEX_SINGLE_ASTERISK = /\*/g; 13 | const REPLACE_SINGLE_ASTERISK = '([^\\/]+)'; // unnamed capture group 14 | 15 | function patternToRegExp(pattern) { 16 | const matcherString = pattern 17 | .replace(REGEX_CAPTURE_GROUPS, REPLACE_CAPTURE_GROUPS) 18 | .replace(REGEX_DOUBLE_ASTERISK, REPLACE_DOUBLE_ASTERISK) 19 | .replace(REGEX_SINGLE_ASTERISK, REPLACE_SINGLE_ASTERISK) 20 | .replace(/\//g, '\\\/'); 21 | return new RegExp(`^${matcherString}$`); 22 | } 23 | 24 | const routeRegex = patternToRegExp(_route); 25 | 26 | function parseParamsFromUrlPath(url) { 27 | const { pathname } = new URL(url); 28 | const {groups} = routeRegex.exec(pathname) || {}; 29 | return groups; 30 | } 31 | 32 | class Context { 33 | constructor(url) { 34 | this[urlSym] = url; 35 | } 36 | 37 | get query() { 38 | return this[paramsSym] || (this[paramsSym] = new URL(this[urlSym]).searchParams); 39 | } 40 | 41 | get params() { 42 | return this[querySym] || (this[querySym] = parseParamsFromUrlPath(this[urlSym])); 43 | } 44 | 45 | } 46 | 47 | export function generateContextObject(url) { 48 | return new Context(url); 49 | } 50 | -------------------------------------------------------------------------------- /js/bootstrap/fetch.js: -------------------------------------------------------------------------------- 1 | import FormData from 'internal:form_data.js'; 2 | import Response from 'internal:response.js'; 3 | import Request from 'internal:request.js'; 4 | 5 | const { 6 | setFetchHandler, 7 | _fetch 8 | } = self._bindings; 9 | 10 | const fetchCbs = {}; 11 | function handleFetch(err, body, meta, fetchId) { 12 | fetchCbs[fetchId](err, body, meta); 13 | } 14 | setFetchHandler(handleFetch); 15 | 16 | // https://tools.ietf.org/html/rfc1867 17 | // https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html#z0 18 | function generateMultipartFormData(formData) { 19 | const num = Math.floor(Math.random() * 899999999) + 100000000; 20 | const boundary = `--------------OsgoodFormBoundary${num}`; 21 | 22 | let body = ''; 23 | 24 | for (let entry of formData) { 25 | if (entry[2]) { 26 | throw new TypeError("Osgood currently doesn't support files"); 27 | } 28 | body += `--${boundary}\r\n`; 29 | body += `Content-Disposition: form-data; name="${entry[0]}"\r\n`; 30 | body += `\r\n`; 31 | body += `${entry[1]}\r\n`; 32 | } 33 | 34 | body += `--${boundary}--\r\n`; 35 | 36 | return { 37 | body, 38 | contentType: `multipart/form-data; boundary=${boundary}` 39 | }; 40 | } 41 | 42 | let increasingFetchId = 0; 43 | 44 | export default async function fetch(input, init) { 45 | const fetchId = ++increasingFetchId; 46 | const p = new Promise((resolve, reject) => { 47 | let writer = null; 48 | fetchCbs[fetchId] = (err, data, meta) => { 49 | if (err) { 50 | err = new Error(err); 51 | reject(err); 52 | console.error('rejected fetch call due to: ' + err); 53 | return; 54 | } 55 | if (meta) { 56 | const readable = new ReadableStream({ 57 | start(controller) { 58 | writer = controller; 59 | } 60 | }); 61 | resolve(new Response(readable, meta)); 62 | } else if (data === null) { 63 | writer.close(); 64 | delete fetchCbs[fetchId]; 65 | } else { 66 | if (data instanceof ArrayBuffer) { 67 | data = new Uint8Array(data); 68 | } 69 | writer.enqueue(data); 70 | } 71 | }; 72 | }); 73 | 74 | if (typeof input === 'string') { 75 | input = new Request(input, init); 76 | } 77 | 78 | const url = input.url; 79 | if (!url.startsWith('http:') && !url.startsWith('https:')) { 80 | throw new TypeError(`Unsupported protocol: "${url.split(':')[0]}:"`); 81 | } 82 | const headers = input.headers; 83 | const method = input.method.toUpperCase(); 84 | 85 | if (typeof input._bodyString === 'string') { 86 | _fetch(url, headers, method, input._bodyString, fetchId, 'string'); 87 | } else if (typeof input.body === 'object') { 88 | if (input.body instanceof FormData) { 89 | const { contentType, body } = generateMultipartFormData(input.body); 90 | headers.set('Content-Type', contentType); 91 | _fetch(url, headers, method, body, fetchId, 'string'); 92 | } else { 93 | _fetch(url, headers, method, null, fetchId, 'stream'); 94 | for await (const chunk of input.body) { 95 | _fetch(null, null, null, chunk, fetchId, 'stream'); 96 | } 97 | _fetch(null, null, null, false, fetchId, 'stream'); 98 | } 99 | } else { 100 | _fetch(url, headers, method, null, fetchId, 'none'); 101 | } 102 | 103 | return p; 104 | } 105 | -------------------------------------------------------------------------------- /js/bootstrap/form_data.js: -------------------------------------------------------------------------------- 1 | const dataSym = _bindings.getPrivate('data'); 2 | 3 | export default class FormData { 4 | 5 | // TODO: This could be a Map for efficient lookups 6 | // However, there shouldn't be more than a dozen entries 7 | // Duplicate names are allowed to exist otherwise it could be a simple Map 8 | //#data = []; // using `dataSym` 9 | 10 | constructor(form) { 11 | if (form) { 12 | throw new TypeError("Osgood FormData doesn't support a form argument"); 13 | } 14 | this[dataSym] = []; 15 | } 16 | 17 | // FormData can have duplicate entries 18 | append(name, value, filename) { 19 | if (filename) { 20 | // TODO: if (!(value instanceof Blob) && !(value instanceof File)) { value = String(value); } 21 | // TODO: There's some more logic about extracting filename from File arg, and defaulting filename to 'blob' 22 | throw new TypeError("Osgood currently doesn't support files"); 23 | } 24 | 25 | const d = [name, value]; 26 | 27 | // if (typeof filename !== 'undefined') { 28 | // d.push(String(filename)); 29 | // } 30 | 31 | this[dataSym].push(d); 32 | } 33 | 34 | // destroys all existing entries with same name 35 | set(name, value, filename) { 36 | this.delete(name); 37 | this.append(name, value, filename); 38 | } 39 | 40 | // destroys all existing entries with same name 41 | delete(name) { 42 | const new_data = []; 43 | 44 | for (let entry of this[dataSym]) { 45 | if (entry[0] !== name) { 46 | new_data.push(entry); 47 | } 48 | } 49 | 50 | this[dataSym] = new_data; 51 | } 52 | 53 | // get first entry of `name` 54 | get(name) { 55 | for (let entry of this[dataSym]) { 56 | if (entry[0] === name) { 57 | return entry[1]; 58 | } 59 | } 60 | } 61 | 62 | // get array of entries of `name` 63 | getAll(name) { 64 | const matches = []; 65 | 66 | for (let entry of this[dataSym]) { 67 | if (entry[0] === name) { 68 | matches.push(entry[1]); 69 | } 70 | } 71 | 72 | return matches; 73 | } 74 | 75 | has(name) { 76 | for (let entry of this[dataSym]) { 77 | if (entry[0] === name) { 78 | return true 79 | } 80 | } 81 | 82 | return false; 83 | } 84 | 85 | entries() { 86 | return this[dataSym][Symbol.iterator](); 87 | } 88 | 89 | [Symbol.iterator]() { 90 | return this[dataSym][Symbol.iterator](); 91 | } 92 | 93 | // iterator 94 | *keys() { 95 | for (let entry of this[dataSym]) { 96 | yield entry[0]; 97 | } 98 | } 99 | 100 | // iterator 101 | *values() { 102 | for (let entry of this[dataSym]) { 103 | yield entry[1]; 104 | } 105 | } 106 | 107 | // not in the spec but Firefox and Chrome have it 108 | forEach(fn) { 109 | for (let entry of this[dataSym]) { 110 | fn(entry[0], entry[1], this); 111 | } 112 | } 113 | 114 | toString() { 115 | return '[object FormData]'; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /js/bootstrap/headers.js: -------------------------------------------------------------------------------- 1 | import { unimplemented, IteratorMixin } from 'internal:common.js'; 2 | 3 | export default class Headers { 4 | constructor(init = {}) { 5 | // TODO This really should be private and not exposed to user code. It's 6 | // exposed for now to more easily pass it into native code. In the future, 7 | // we can just call `entries()` to get the underlying headers. 8 | this._headers = {}; 9 | if (init instanceof Headers) { 10 | for (const [name, value] of Headers.prototype.keys.apply(init)) { 11 | this.append(name, value); 12 | } 13 | } else if (typeof init === 'object') { 14 | if (Symbol.iterator in init) { 15 | for (const header of init) { 16 | if (typeof header !== 'object' || !(Symbol.iterator in header)) { 17 | throw new TypeError('Invalid headers'); 18 | } 19 | let [name, value, ...extras] = [...header]; 20 | if (extras.length !== 0) { 21 | throw new TypeError('Invalid headers'); 22 | } 23 | this.append(name, value); 24 | } 25 | } else { 26 | for (const name in init) { 27 | if (Object.prototype.hasOwnProperty.call(init, name)) { 28 | this.append(name, init[name]); 29 | } 30 | } 31 | } 32 | } else { 33 | throw new TypeError('Invalid headers'); 34 | } 35 | } 36 | 37 | set(name, value) { 38 | name = normalizeHeaderName(name); 39 | value = normalizeHeaderValue(value); 40 | this._headers[name] = String(value); 41 | } 42 | 43 | append(name, value) { 44 | name = normalizeHeaderName(name); 45 | value = normalizeHeaderValue(value); 46 | if (name in this._headers) { 47 | this._headers[name] += ', ' + value; 48 | } else { 49 | this._headers[name] = value; 50 | } 51 | } 52 | 53 | get(name) { 54 | name = normalizeHeaderName(name); 55 | return this._headers[name]; 56 | } 57 | 58 | has(name) { 59 | name = normalizeHeaderName(name); 60 | return name in this._headers; 61 | } 62 | 63 | delete(name) { 64 | name = normalizeHeaderName(name); 65 | delete this._headers[name]; 66 | } 67 | 68 | *[Symbol.iterator]() { 69 | yield* Object.entries(this._headers); 70 | } 71 | } 72 | IteratorMixin.mixin(Headers); 73 | 74 | // https://tools.ietf.org/html/rfc7230#section-3.2.6 75 | const headerNameRe = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; 76 | function normalizeHeaderName(name) { 77 | if (!name) { 78 | throw new TypeError('Invalid header name'); 79 | } 80 | name = String(name).toLowerCase(); 81 | if (!headerNameRe.test(name)) { 82 | throw new TypeError('Invalid header name: ' + name); 83 | } 84 | return name; 85 | } 86 | 87 | // https://tools.ietf.org/html/rfc7230#section-3.2 88 | const invalidHeaderValueRe = /[^\t\x20-\x7e\x80-\xff]/; 89 | function normalizeHeaderValue(value) { 90 | if (value === undefined) { 91 | throw new TypeError('Invalid header value'); 92 | } 93 | value = String(value); 94 | if (invalidHeaderValueRe.test(value)) { 95 | throw new TypeError('Invalid header value: ' + value); 96 | } 97 | return value; 98 | } 99 | -------------------------------------------------------------------------------- /js/bootstrap/inbound.js: -------------------------------------------------------------------------------- 1 | import { generateContextObject } from 'internal:context.js'; 2 | import { isBufferish } from 'internal:common.js'; 3 | import Headers from 'internal:headers.js'; 4 | import Response from 'internal:response.js'; 5 | import Request from 'internal:request.js'; 6 | import { BodyMixin, writeChunk, writeChunkSym } from 'internal:body_mixin.js'; 7 | 8 | const { 9 | sendError, 10 | startResponse, 11 | writeResponse, 12 | stringResponse, 13 | setFetchHandler, 14 | setIncomingReqHeadHandler, 15 | } = self._bindings; 16 | 17 | // This function checks to see if the object should serialize into a POJO 18 | // Object, one that is free of class instances. "Double getters" do exist. 19 | // For example, it could first reply wth a string, and later reply a class 20 | // instance. Keep in mind this check is done to prevent a foot gun, not for 21 | // security purposes. If it were for security we'd construct a shadow object 22 | // and copy properties. Double Getter's are explained here: 23 | // https://medium.com/intrinsic/protecting-your-javascript-apis-9ce5b8a0e3b5 24 | function shouldSerializeIntoPOJO(obj) { 25 | if (obj === null) { 26 | return true; 27 | } else if (typeof obj !== 'object') { 28 | return true; 29 | } 30 | 31 | if (obj.toJSON) { 32 | obj = obj.toJSON(); 33 | } 34 | 35 | if (obj === null) { 36 | return true; 37 | } else if (typeof obj !== 'object') { 38 | return true; 39 | } 40 | 41 | const proto = Object.getPrototypeOf(obj); 42 | 43 | if (proto === Array.prototype) { 44 | for (let value of obj) { 45 | if (!shouldSerializeIntoPOJO(value)) { 46 | return false; 47 | } 48 | } 49 | return true; 50 | } else if (proto !== Object.prototype && proto !== null) { 51 | return false; 52 | } else { 53 | // intentionally ignore Symbol properties as they're ignored by JSON.stringify 54 | for (let value of Object.values(obj)) { 55 | if (!shouldSerializeIntoPOJO(value)) { 56 | return false; 57 | } 58 | } 59 | return true; 60 | } 61 | } 62 | 63 | function incomingReqHeadHandler(reqId, fn, method, url, headers) { 64 | const request = new Request(url, { 65 | method, 66 | headers, 67 | body: writeChunkSym 68 | }); 69 | (async () => { 70 | try { 71 | if (typeof fn !== 'function') { 72 | throw new TypeError('Worker did not provide a valid handler'); 73 | } 74 | 75 | await getResponse(reqId, fn, url, request); 76 | } catch (e) { 77 | console.error(e.stack); 78 | sendError(500, '', reqId); 79 | } 80 | })(); 81 | return async function handleIncomingReqBody(body) { 82 | writeChunk.call(request, body); 83 | }; 84 | } 85 | setIncomingReqHeadHandler(incomingReqHeadHandler); 86 | 87 | function isPromise(p) { 88 | return typeof p === 'object' && p !== null && typeof p.then === 'function'; 89 | } 90 | 91 | async function getResponse(reqId, fn, url, request) { 92 | let response = fn(request, generateContextObject(url)); 93 | if (isPromise(response)) { 94 | response = await response; 95 | } 96 | 97 | switch (typeof response) { 98 | case 'string': { 99 | // handle it in native code 100 | stringResponse(response, reqId); 101 | return; 102 | } 103 | case 'object': { 104 | if (response === null) { 105 | throw new TypeError('Response was an invalid object'); 106 | } 107 | if (response instanceof Response) { 108 | // we're good! 109 | } else if (isBufferish(response)) { 110 | response = new Response(response, { 111 | headers: new Headers({ 112 | 'Content-Type': 'application/octet-stream' 113 | }) 114 | }); 115 | } else { 116 | if (shouldSerializeIntoPOJO(response)) { 117 | const body = JSON.stringify(response); 118 | response = new Response(body, { 119 | headers: new Headers({ 120 | 'Content-Type': 'application/json' 121 | }) 122 | }); 123 | } else { 124 | throw new TypeError('Response object must be a POJO'); 125 | } 126 | } 127 | break; 128 | } 129 | default: 130 | throw new TypeError(`Invalid response type "${typeof response}"`); 131 | } 132 | 133 | if (response.body) { 134 | startResponse(response, reqId); 135 | let stream = 136 | response.body instanceof TransformStream 137 | ? response.body.readable 138 | : response.body; 139 | for await (let chunk of stream) { 140 | writeResponse(chunkAsArrayBuffer(chunk), reqId); 141 | } 142 | writeResponse(null, reqId); 143 | } else { 144 | startResponse(response, reqId, response._bodyString); 145 | } 146 | } 147 | 148 | function chunkAsArrayBuffer(chunk) { 149 | if (!(chunk instanceof ArrayBuffer)) { 150 | if (typeof chunk === 'string') { 151 | const enc = new TextEncoder(); 152 | chunk = enc.encode(chunk).buffer; 153 | return chunk; 154 | } 155 | if (typeof chunk === 'object') { 156 | if (chunk.buffer && chunk.buffer instanceof ArrayBuffer) { 157 | chunk = chunk.buffer; 158 | } else { 159 | throw new TypeError( 160 | 'body chunks must be strings, ArrayBuffers, TypedArrays or DataViews' 161 | ); 162 | } 163 | } 164 | } 165 | return chunk; 166 | } 167 | -------------------------------------------------------------------------------- /js/bootstrap/index.js: -------------------------------------------------------------------------------- 1 | import FormData from 'internal:form_data.js'; 2 | import Headers from 'internal:headers.js'; 3 | import Response from 'internal:response.js'; 4 | import Request from 'internal:request.js'; 5 | import { atob, btoa } from 'internal:base64.js'; 6 | import { setInterval, setTimeout, clearTimeout } from 'internal:timers.js'; 7 | import fetch from 'internal:fetch.js'; 8 | import 'internal:inbound.js'; 9 | import 'internal:console.js'; 10 | 11 | delete self._bindings; 12 | 13 | Object.assign(self, { 14 | FormData, 15 | Headers, 16 | Response, 17 | Request, 18 | atob, 19 | btoa, 20 | setInterval, 21 | setTimeout, 22 | clearInterval: clearTimeout, 23 | clearTimeout, 24 | fetch, 25 | }); 26 | -------------------------------------------------------------------------------- /js/bootstrap/request.js: -------------------------------------------------------------------------------- 1 | import { BodyMixin } from 'internal:body_mixin.js'; 2 | 3 | const { getPrivate } = _bindings; 4 | 5 | const urlSym = getPrivate('url'); 6 | const methodSym = getPrivate('method'); 7 | 8 | 9 | const bodyInit = BodyMixin.init; 10 | 11 | export default class Request { 12 | // #url; 13 | // #method; 14 | constructor(input, init = {}) { 15 | // TODO support `input` being a Request 16 | this[urlSym] = input; 17 | this[methodSym] = init.method || 'GET'; 18 | bodyInit.call(this, init.body, init); 19 | } 20 | 21 | get url() { 22 | return this[urlSym]; 23 | } 24 | 25 | get method() { 26 | return this[methodSym]; 27 | } 28 | } 29 | BodyMixin.mixin(Request); 30 | -------------------------------------------------------------------------------- /js/bootstrap/response.js: -------------------------------------------------------------------------------- 1 | import { BodyMixin } from 'internal:body_mixin.js'; 2 | 3 | const { getPrivate } = _bindings; 4 | 5 | const statusSym = getPrivate('status'); 6 | const statusTextSym = getPrivate('statusText'); 7 | 8 | const bodyInit = BodyMixin.init; 9 | 10 | export default class Response { 11 | // #status; 12 | // #statusText; 13 | constructor(body, init = {}) { 14 | this[statusSym] = init.status || 200; 15 | this[statusTextSym] = init.statusText || 'OK'; 16 | bodyInit.call(this, body, init); 17 | } 18 | 19 | get status() { 20 | return this[statusSym]; 21 | } 22 | 23 | get statusText() { 24 | return this[statusTextSym]; 25 | } 26 | } 27 | BodyMixin.mixin(Response); 28 | -------------------------------------------------------------------------------- /js/bootstrap/timers.js: -------------------------------------------------------------------------------- 1 | const { 2 | setTimerHandler, 3 | setTimeout: _setTimeout, 4 | setInterval: _setInterval, 5 | clearTimer 6 | } = self._bindings; 7 | 8 | let timerIdCounter = 0; 9 | const timerMap = new Map(); 10 | let timerNestingLevel = 0; 11 | 12 | function handleTimer(timerId) { 13 | timerMap.get(timerId)(); 14 | } 15 | setTimerHandler(handleTimer); 16 | 17 | 18 | function normalizeTimeout(timeout) { 19 | timeout = Number(timeout); 20 | 21 | return (timeout >= 0 ? timeout : 0); 22 | } 23 | 24 | // Implementation is based on the following specification: 25 | // https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers 26 | // A few adjustments/assumptions were made: 27 | // - The global scope will behave like a `WorkerGlobalScope` 28 | // - The method `HostEnsureCanCompileStrings` will throw an exception 29 | // - Since the WHATWG spec appears to be ambiguous about valid types for 30 | // `timeout`, it will first be cast to an ECMAScript Number using the 31 | // Number constructor, and then values of `NaN` will be treated as `0` 32 | // 33 | // TODO(perf): We can avoid making this function megamorphic by performing 34 | // typechecking in both `setInterval` and `setTimeout`, which is probably 35 | // worth doing if this is a hot path 36 | function setTimer(id, handler, timeout, nestingLevel, args, repeating) { 37 | timeout = normalizeTimeout(timeout); 38 | 39 | // Throttle timeout values 40 | if (nestingLevel > 5 && timeout < 4) { 41 | timeout = 4; 42 | } 43 | 44 | // Handler can be any type, but we don't currently support string 45 | // compilation, and all other non-function types will get casted to 46 | // strings anyway 47 | if (typeof handler !== 'function') { 48 | throw new Error('Dynamic string compilation is currently unsupported'); 49 | } 50 | 51 | timerMap.set(id, () => { 52 | timerNestingLevel = nestingLevel + 1; 53 | try { 54 | handler.apply(null, args); 55 | } catch (err) { 56 | console.error(err && typeof err === 'object' && err.stack ? err.stack : String(err)); 57 | } 58 | 59 | if (repeating) { 60 | setTimer(id, handler, timeout, timerNestingLevel, args, repeating); 61 | } 62 | 63 | timerNestingLevel = nestingLevel; 64 | }); 65 | 66 | if (repeating && nestingLevel > 5) { 67 | // Micro-optimization to switch to native tokio interval handler after backoff 68 | repeating = false; 69 | _setInterval(id, timeout); 70 | } else { 71 | _setTimeout(id, timeout); 72 | } 73 | } 74 | 75 | export function setInterval(handler, timeout, ...args) { 76 | const id = timerIdCounter++; 77 | setTimer(id, handler, timeout, timerNestingLevel, args, true); 78 | return id; 79 | } 80 | 81 | export function setTimeout(handler, timeout, ...args) { 82 | const id = timerIdCounter++; 83 | setTimer(id, handler, timeout, timerNestingLevel, args, false); 84 | return id; 85 | } 86 | 87 | export function clearTimeout(id) { 88 | timerMap.delete(id); 89 | clearTimer(id); 90 | } 91 | -------------------------------------------------------------------------------- /js/config_bootstrap.js: -------------------------------------------------------------------------------- 1 | { 2 | const formatLog = args => 3 | args.map(x => (typeof x === 'string' ? x : JSON.stringify(x))).join(' '); 4 | 5 | console.log = (...args) => { 6 | _log(formatLog(args)); 7 | }; 8 | 9 | console.error = (...args) => { 10 | _error(formatLog(args)); 11 | }; 12 | 13 | console.warn = (...args) => { 14 | _error(formatLog(args)); 15 | }; 16 | 17 | console.info = (...args) => { 18 | _log(formatLog(args)); 19 | }; 20 | 21 | console.debug = (...args) => { 22 | _log(formatLog(args)); 23 | }; 24 | 25 | console.trace = (...args) => { 26 | const { stack } = new Error(); 27 | const formattedStack = stack 28 | .split('\n') 29 | .slice(2) 30 | .join('\n'); 31 | _log(`${formatLog(args)}\n${formattedStack}`); 32 | }; 33 | 34 | const httpMethods = [ 35 | 'Get', 36 | 'Post', 37 | 'Put', 38 | 'Patch', 39 | 'Delete', 40 | 'Head', 41 | 'Options', 42 | 'Trace', 43 | 'Connect' 44 | ]; 45 | 46 | Object.defineProperty(this, 'app', { 47 | value: {}, 48 | writable: false, 49 | enumerable: true, 50 | configurable: false 51 | }); 52 | 53 | // port defaults to 8080 54 | let port = 8080; 55 | Reflect.defineProperty(app, 'port', { 56 | get: () => port, 57 | set(p) { 58 | if (!Number.isInteger(p) || p < 0) { 59 | throw new Error('port must be a valid port number'); 60 | } 61 | port = p; 62 | }, 63 | enumerable: true, 64 | configurable: false 65 | }); 66 | 67 | // interface defaults to 0.0.0.0 68 | let interface = '0.0.0.0'; 69 | Reflect.defineProperty(app, 'interface', { 70 | get: () => interface, 71 | set(i) { 72 | if (typeof i !== 'string') { 73 | throw new Error('interface must be a valid IP address'); 74 | } 75 | interface = i; 76 | }, 77 | enumerable: true, 78 | configurable: false 79 | }); 80 | 81 | // host defaults to localhost 82 | app.host = 'localhost'; 83 | 84 | app.routes = []; 85 | app.staticRoutes = []; 86 | 87 | app.static = (routePrefix, directory, options = {}) => { 88 | if (typeof routePrefix !== 'string') { 89 | throw new TypeError('routePrefix must be a string'); 90 | } 91 | 92 | if (routePrefix.endsWith('/')) { 93 | routePrefix = routePrefix.substring(0, routePrefix.length - 1); 94 | } 95 | 96 | if (typeof directory !== 'string') { 97 | throw new TypeError('directory must be a string'); 98 | } 99 | 100 | if (directory.endsWith('/')) { 101 | directory = directory.substring(0, directory.length - 1); 102 | } 103 | 104 | app.staticRoutes.push({ routePrefix, directory, options }); 105 | }; 106 | 107 | const formatRoute = route => { 108 | const formattedRoute = route.replace(/\:([a-zA-Z0-9_]+)/g, '*'); 109 | return formattedRoute; 110 | }; 111 | 112 | // TODO: method should also accept an array 113 | app.route = (method, route, worker, policyFn = () => {}) => { 114 | const policyWriter = { 115 | outboundHttp: {} 116 | }; 117 | const policies = []; 118 | for (const method of httpMethods) { 119 | policyWriter.outboundHttp[`allow${method}`] = pattern => { 120 | // TODO check if pattern is formatted correctly (no hashes or query params) 121 | policies.push({ method: method.toUpperCase(), pattern }); 122 | }; 123 | } 124 | policyFn(policyWriter); 125 | app.routes.push({ 126 | method, 127 | pattern: formatRoute(route), 128 | rawPattern: route, 129 | file: worker, 130 | policies 131 | }); 132 | }; 133 | 134 | // Syntax Sugar 135 | for (const method of httpMethods) { 136 | app[method.toLowerCase()] = (route, worker, policyFn) => { 137 | app.route(method.toUpperCase(), route, worker, policyFn); 138 | }; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osgood", 3 | "version": "1.0.0", 4 | "description": "Server-side Service Workers", 5 | "main": "bootstrap.js", 6 | "scripts": { 7 | "install": "mkdir -p vendor && cd vendor && test -d streams || git clone https://github.com/whatwg/streams", 8 | "postinstall": "webpack", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "Intrinsic ", 12 | "license": "UNLICENSED", 13 | "devDependencies": { 14 | "webpack": "^4.29.6", 15 | "webpack-cli": "^3.2.3", 16 | "whatwg-url": "^7.0.0" 17 | }, 18 | "dependencies": { 19 | "fast-text-encoding": "^1.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /js/preamble.js: -------------------------------------------------------------------------------- 1 | { 2 | const window = {}; 3 | self.window = window; 4 | self.gc = () => {}; 5 | require('./vendor/streams/reference-implementation/lib'); 6 | require('fast-text-encoding/text.js'); 7 | const { URL, URLSearchParams } = require('whatwg-url'); 8 | self.ReadableStream = window.ReadableStream; 9 | self.WritableStream = window.WritableStream; 10 | self.TransformStream = window.TransformStream; 11 | self.TextEncoder = window.TextEncoder; 12 | self.TextDecoder = window.TextDecoder; 13 | self.URL = URL; 14 | self.URLSearchParams = URLSearchParams; 15 | delete self.window; 16 | delete self.gc; 17 | } 18 | -------------------------------------------------------------------------------- /js/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | entry: './preamble.js', 4 | mode: process.env.NODE_ENV || 'development', 5 | output: { 6 | filename: 'preamble.js', 7 | path: path.join(__dirname, 'dist'), 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /osgood-v8-macros/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "proc-macro2" 5 | version = "0.4.28" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | dependencies = [ 8 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 9 | ] 10 | 11 | [[package]] 12 | name = "quote" 13 | version = "0.6.12" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | dependencies = [ 16 | "proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)", 17 | ] 18 | 19 | [[package]] 20 | name = "syn" 21 | version = "0.15.32" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | dependencies = [ 24 | "proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)", 25 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 26 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 27 | ] 28 | 29 | [[package]] 30 | name = "unicode-xid" 31 | version = "0.1.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | 34 | [[package]] 35 | name = "v8-macros" 36 | version = "0.1.0" 37 | dependencies = [ 38 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 39 | "syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)", 40 | ] 41 | 42 | [metadata] 43 | "checksum proc-macro2 0.4.28 (registry+https://github.com/rust-lang/crates.io-index)" = "ba92c84f814b3f9a44c5cfca7d2ad77fa10710867d2bbb1b3d175ab5f47daa12" 44 | "checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db" 45 | "checksum syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)" = "846620ec526c1599c070eff393bfeeeb88a93afa2513fc3b49f1fea84cf7b0ed" 46 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 47 | -------------------------------------------------------------------------------- /osgood-v8-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "osgood-v8-macros" 3 | version = "0.2.1" 4 | authors = ["Intrinsic "] 5 | edition = "2018" 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | quote = "0.6.12" 12 | osgood-v8 = { path = "../osgood-v8" } 13 | 14 | [dependencies.syn] 15 | version = "0.15.32" 16 | features = ["full", "extra-traits"] 17 | -------------------------------------------------------------------------------- /osgood-v8-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | use proc_macro::TokenStream; 3 | use syn; 4 | 5 | #[macro_use] 6 | extern crate quote; 7 | 8 | #[proc_macro_attribute] 9 | pub fn v8_fn(_attr: TokenStream, item: TokenStream) -> TokenStream { 10 | let ast = syn::parse_macro_input!(item as syn::ItemFn); 11 | let name = ast.ident; 12 | let inputs = ast.decl.inputs; 13 | let block = ast.block; 14 | let vis = ast.vis; 15 | 16 | (quote! { 17 | #vis extern "C" fn #name(args: *const osgood_v8::V8::FunctionCallbackInfo) { 18 | let args = osgood_v8::wrapper::FunctionCallbackInfo::new(args); 19 | handle_scope!({ 20 | (|#inputs|#block)(args); 21 | }); 22 | } 23 | }) 24 | .into() 25 | } 26 | -------------------------------------------------------------------------------- /osgood-v8-macros/tests/smoke.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate osgood_v8; 3 | use osgood_v8::wrapper::*; 4 | 5 | #[osgood_v8_macros::v8_fn] 6 | fn wrapped_function(context: FunctionCallbackInfo) {} 7 | 8 | #[osgood_v8_macros::v8_fn] 9 | pub fn pub_wrapped_function(context: FunctionCallbackInfo) {} 10 | -------------------------------------------------------------------------------- /osgood-v8/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "osgood-v8" 3 | version = "0.2.1" 4 | authors = ["Intrinsic "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | 9 | [build-dependencies] 10 | bindgen = "0.42.2" 11 | cc = "1.0.35" 12 | git2 = "0.8.0" 13 | -------------------------------------------------------------------------------- /osgood-v8/build.rs: -------------------------------------------------------------------------------- 1 | use git2::{ErrorCode, Repository}; 2 | 3 | use bindgen; 4 | use cc; 5 | 6 | use std::env; 7 | use std::path::PathBuf; 8 | use std::process::Command; 9 | 10 | fn main() { 11 | let v8_dir = match env::var("CUSTOM_V8") { 12 | Ok(custom_v8_dir) => { 13 | let custom_v8_dir = PathBuf::from(custom_v8_dir); 14 | assert!(custom_v8_dir.exists()); 15 | custom_v8_dir 16 | } 17 | Err(_) => build_v8(), 18 | }; 19 | 20 | compile_wrappers(v8_dir.clone()); 21 | generate_bindings(v8_dir); 22 | } 23 | 24 | fn build_v8() -> PathBuf { 25 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 26 | let v8_repo_path = out_dir.clone().join("third_party/v8"); 27 | 28 | if v8_repo_path.exists() { 29 | return v8_repo_path; 30 | } 31 | 32 | // Fetch the v8 source 33 | fetch_v8(out_dir.clone()); 34 | 35 | // Checkout the correct v8 version 36 | let status = Command::new("git") 37 | .args(&["checkout", include_str!("v8-version.txt").trim()]) 38 | .current_dir(v8_repo_path.clone()) 39 | .status() 40 | .expect("Failed to checkout correct v8 version"); 41 | assert!(status.success()); 42 | 43 | // Update third-party repos and run pre-compile hooks 44 | let status = Command::new("gclient") 45 | .arg("sync") 46 | .current_dir(v8_repo_path.clone()) 47 | .status() 48 | .expect("Failed to synchronize gclient deps"); 49 | assert!(status.success()); 50 | 51 | // Build v8 52 | generate_config(out_dir.clone()); 53 | run_ninja(out_dir.clone()); 54 | 55 | v8_repo_path 56 | } 57 | 58 | fn compile_wrappers(v8_dir: PathBuf) { 59 | let include_dir = v8_dir.join("include"); 60 | 61 | println!("cargo:rerun-if-changed=src/wrapper.cpp"); 62 | 63 | cc::Build::new() 64 | .cpp(true) 65 | .warnings(false) 66 | .flag("--std=c++14") 67 | .include(include_dir) 68 | .file("src/wrapper.cpp") 69 | .compile("libwrapper.a"); 70 | } 71 | 72 | fn generate_bindings(v8_dir: PathBuf) { 73 | println!("cargo:rustc-link-lib=v8_libbase"); 74 | println!("cargo:rustc-link-lib=v8_libplatform"); 75 | println!("cargo:rustc-link-lib=v8_monolith"); 76 | println!("cargo:rustc-link-lib=c++"); 77 | println!( 78 | "cargo:rustc-link-search={}/out.gn/x64.release/obj", 79 | v8_dir.to_str().unwrap() 80 | ); 81 | println!( 82 | "cargo:rustc-link-search={}/out.gn/x64.release/obj/third_party/icu", 83 | v8_dir.to_str().unwrap() 84 | ); 85 | 86 | let bindings = bindgen::Builder::default() 87 | .generate_comments(true) 88 | .header("src/wrapper.cpp") 89 | .rust_target(bindgen::RustTarget::Nightly) 90 | .clang_arg("-x") 91 | .clang_arg("c++") 92 | .clang_arg("--std=c++14") 93 | .clang_arg(format!("-I{}", v8_dir.join("include").to_str().unwrap())) 94 | // Because there are some layout problems with these 95 | .opaque_type("std::.*") 96 | .whitelist_type("std::unique_ptr\\") 97 | .whitelist_type("v8::.*") 98 | .blacklist_type("std::basic_string.*") 99 | .whitelist_function("v8::.*") 100 | .whitelist_function("osgood::.*") 101 | .whitelist_var("v8::.*") 102 | // Re-structure the modules a bit and hide the "root" module 103 | .raw_line("#[doc(hidden)]") 104 | // .generate_inline_functions(true) 105 | .enable_cxx_namespaces() 106 | .derive_debug(true) 107 | .derive_hash(true) 108 | .derive_eq(true) 109 | .derive_partialeq(true) 110 | .rustfmt_bindings(true) // comment this for a slightly faster build 111 | .generate() 112 | .expect("Unable to generate bindings"); 113 | 114 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 115 | bindings 116 | .write_to_file(out_path.join("bindings.rs")) 117 | .expect("Couldn't write bindings!"); 118 | } 119 | 120 | fn fetch_v8(out_dir: PathBuf) { 121 | let v8_repo_path = out_dir.join("third_party/v8"); 122 | 123 | if !v8_repo_path.exists() { 124 | let res = Command::new("fetch") 125 | .arg("v8") 126 | .current_dir(out_dir.join("third_party")) 127 | .status(); 128 | let status = match res { 129 | Ok(val) => val, 130 | Err(_) => { 131 | download_depot_tools(out_dir.clone()); 132 | Command::new("fetch") 133 | .arg("v8") 134 | .current_dir(out_dir.join("third_party")) 135 | .status() 136 | .unwrap() 137 | } 138 | }; 139 | assert!(status.success()); 140 | } 141 | } 142 | 143 | fn download_depot_tools(out_dir: PathBuf) { 144 | let depot_tools_repo_url = "https://chromium.googlesource.com/chromium/tools/depot_tools.git"; 145 | let depot_tools_repo_path = out_dir.join("third_party/depot_tools"); 146 | 147 | // Clone the depot_tools repo 148 | match Repository::clone(depot_tools_repo_url, depot_tools_repo_path.clone()) { 149 | Ok(_) => (), 150 | Err(ref e) if e.code() == ErrorCode::Exists => (), 151 | Err(e) => panic!("Failed to clone depot tools: {}", e), 152 | }; 153 | 154 | // Set the path 155 | if let Some(path) = env::var_os("PATH") { 156 | let mut paths = env::split_paths(&path).collect::>(); 157 | paths.push(depot_tools_repo_path.clone()); 158 | let new_path = env::join_paths(paths).unwrap(); 159 | env::set_var("PATH", &new_path); 160 | } 161 | } 162 | 163 | fn generate_config(out_dir: PathBuf) { 164 | let v8_repo_path = out_dir.join("third_party/v8"); 165 | 166 | let status = Command::new("tools/dev/v8gen.py") 167 | .args(&[ 168 | "x64.release", 169 | "--", 170 | "v8_monolithic=true", 171 | "v8_use_external_startup_data=false", 172 | "use_custom_libcxx=false", 173 | ]) 174 | .current_dir(v8_repo_path) 175 | .status() 176 | .expect("Failed to generate v8 build configuration"); 177 | assert!(status.success()); 178 | } 179 | 180 | fn run_ninja(out_dir: PathBuf) { 181 | let ninja_config_path = out_dir.join("third_party/v8/out.gn/x64.release"); 182 | 183 | let status = Command::new("ninja") 184 | .current_dir(ninja_config_path) 185 | .status() 186 | .expect("Failed to compile v8"); 187 | 188 | assert!(status.success()); 189 | } 190 | -------------------------------------------------------------------------------- /osgood-v8/src/binding.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(dead_code)] 5 | #![allow(clippy::all)] 6 | 7 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 8 | 9 | //pub use self::root::__BindgenBitfieldUnit; 10 | //pub use self::root::std as cppstd; 11 | pub use self::root::v8 as V8; 12 | //pub use self::root::FILE; 13 | pub use self::root::osgood; 14 | -------------------------------------------------------------------------------- /osgood-v8/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | #![allow(dead_code)] // comment this out to see what's defined but not used yet 3 | 4 | mod binding; 5 | pub use binding::osgood; 6 | pub use binding::V8; 7 | 8 | pub mod wrapper; 9 | 10 | /// Creates a Local from any `format!`-able variable 11 | #[macro_export] 12 | macro_rules! v8_str { 13 | ( $val:expr ) => { 14 | // TODO should this really be a reference?? 15 | &$crate::wrapper::String::new_from_slice($val) 16 | }; 17 | } 18 | 19 | #[macro_export] 20 | macro_rules! isolate_scope { 21 | ( $isolate:expr, $code:block ) => { 22 | $isolate.enter(); 23 | $code 24 | $isolate.exit(); 25 | $isolate.dispose(); 26 | } 27 | } 28 | 29 | #[macro_export] 30 | macro_rules! handle_scope { 31 | ( $code:block ) => { 32 | let mut scope = $crate::wrapper::HandleScope::new(); 33 | $code 34 | drop(scope); 35 | } 36 | } 37 | 38 | #[macro_export] 39 | macro_rules! context_scope { 40 | ( $context:expr, $code:block ) => { 41 | $context.enter(); 42 | $code 43 | $context.exit(); 44 | } 45 | } 46 | 47 | #[macro_export] 48 | macro_rules! isolate_and_handle_scope { 49 | ( $isolate:expr, $code:block ) => { 50 | isolate_scope!($isolate, { 51 | handle_scope!($code); 52 | }); 53 | }; 54 | } 55 | 56 | #[macro_export] 57 | macro_rules! v8fn { 58 | ($name:ident, $code:expr) => { 59 | extern "C" fn $name(args: *const $crate::V8::FunctionCallbackInfo) { 60 | let args = $crate::wrapper::FunctionCallbackInfo::new(args); 61 | handle_scope!({ 62 | $code(args); 63 | }); 64 | } 65 | }; 66 | (pub $name:ident, $code:expr) => { 67 | pub extern "C" fn $name(args: *const $crate::V8::FunctionCallbackInfo) { 68 | let args = $crate::wrapper::FunctionCallbackInfo::new(args); 69 | handle_scope!({ 70 | $code(args); 71 | }); 72 | } 73 | }; 74 | } 75 | 76 | #[macro_export] 77 | macro_rules! v8_simple_init { 78 | ($code:expr) => { 79 | let isolate = $crate::wrapper::Isolate::new(); 80 | isolate_and_handle_scope!(isolate, { 81 | let mut context = $crate::wrapper::Context::new(); 82 | context_scope!(context, { 83 | $code(context); 84 | }); 85 | }); 86 | }; 87 | } 88 | 89 | #[macro_export] 90 | macro_rules! v8_spawn { 91 | ($code:expr) => { 92 | std::thread::spawn(move || { 93 | let isolate = $crate::wrapper::Isolate::new(); 94 | isolate.enter(); 95 | let scope = $crate::wrapper::HandleScope::new(); 96 | let mut context = $crate::wrapper::Context::new(); 97 | context.enter(); 98 | $code(context); 99 | context.exit(); 100 | drop(scope); 101 | isolate.exit(); 102 | isolate.dispose(); 103 | }); 104 | }; 105 | } 106 | 107 | #[macro_export] 108 | macro_rules! v8_args { 109 | ($($item:expr),+) => { { 110 | let args: Vec<&$crate::wrapper::IntoValue> = vec![$($item),+]; 111 | args 112 | } } 113 | } 114 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/array.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub use V8::Array; 4 | 5 | impl Local { 6 | pub fn length(&mut self) -> i32 { 7 | unsafe { self.inner_mut().Length() as i32 } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/array_buffer.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub use V8::ArrayBuffer; 4 | 5 | use std::ffi::c_void; 6 | use std::ptr::copy_nonoverlapping; 7 | 8 | impl ArrayBuffer { 9 | pub fn new_from_u8_array(arr: &[u8], len: usize) -> Local { 10 | unsafe { 11 | let mut array_buffer = Local::from(ArrayBuffer::New(Isolate::raw(), len)); 12 | let src = arr.as_ptr() as *mut c_void; 13 | copy_nonoverlapping(src, array_buffer.inner_mut().GetContents().data_, len); 14 | array_buffer 15 | } 16 | } 17 | } 18 | 19 | impl Local { 20 | pub fn as_vec_u8(&mut self) -> Vec { 21 | unsafe { 22 | let contents = self.inner_mut().GetContents(); 23 | let arr_ptr = contents.data_ as *mut u8; 24 | let len = contents.byte_length_; 25 | let mut dst = Vec::with_capacity(len); 26 | copy_nonoverlapping(arr_ptr, dst.as_mut_ptr(), len); 27 | dst.set_len(len); 28 | dst 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/context.rs: -------------------------------------------------------------------------------- 1 | use super::{osgood, Isolate, Local, Object, V8}; 2 | 3 | pub use V8::Context; 4 | 5 | impl Context { 6 | // TODO this should be on a wrapper struct rather than the native class 7 | pub fn new() -> Local { 8 | unsafe { osgood::new_context(Isolate::raw()).into() } 9 | } 10 | } 11 | 12 | impl Local { 13 | pub fn global(&mut self) -> Local { 14 | unsafe { self.inner_mut().Global().into() } 15 | } 16 | 17 | pub fn enter(&mut self) { 18 | unsafe { 19 | self.inner_mut().Enter(); 20 | } 21 | } 22 | 23 | pub fn exit(&mut self) { 24 | unsafe { 25 | self.inner_mut().Exit(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/exception.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::convert; 3 | 4 | #[derive(Debug, Copy, Clone)] 5 | pub struct Exception { 6 | exception_: Local, 7 | } 8 | 9 | impl Exception { 10 | pub fn as_rust_string(&mut self) -> std::string::String { 11 | let context = Isolate::get_current_context(); 12 | let mut to_string = self.exception_.get(context, "toString").to_function(); 13 | let result = to_string.call(context, self, vec![]); 14 | result.as_rust_string() 15 | } 16 | 17 | pub fn syntax_error_stack(&mut self) -> std::string::String { 18 | unsafe { 19 | let message = 20 | V8::Exception_CreateMessage(Isolate::raw(), self.exception_.as_value().into()); 21 | let message = message.val_.as_ref().unwrap(); 22 | let origin = message.GetScriptOrigin(); 23 | let name: Local = origin.resource_name_.into(); 24 | let name = name.as_rust_string(); 25 | let context: V8::Local = Isolate::get_current_context().into(); 26 | let line = message.GetLineNumber(context).value_; 27 | let col = message.GetStartColumn1(context).value_; 28 | format!( 29 | "{}\n at {}:{}:{}", 30 | self.as_rust_string(), 31 | name, 32 | line, 33 | col 34 | ) 35 | } 36 | } 37 | } 38 | 39 | impl convert::From> for Exception { 40 | fn from(val: Local) -> Exception { 41 | Exception { exception_: val } 42 | } 43 | } 44 | 45 | impl convert::From> for Exception { 46 | fn from(val: Local) -> Exception { 47 | val.to_object().into() 48 | } 49 | } 50 | 51 | impl convert::From> for Exception { 52 | fn from(val: V8::Local) -> Exception { 53 | let val: Local = val.into(); 54 | val.to_object().into() 55 | } 56 | } 57 | 58 | impl IntoValue for Exception { 59 | fn into_value(&self) -> Local { 60 | self.exception_.as_value() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/function.rs: -------------------------------------------------------------------------------- 1 | use super::{Context, IntoValue, Local, V8}; 2 | 3 | use V8::Function; 4 | 5 | impl Local { 6 | pub fn call( 7 | &mut self, 8 | context: Local, 9 | recv: &IntoValue, 10 | argv: Vec<&IntoValue>, 11 | ) -> Local { 12 | let argc = argv.len() as i32; 13 | let mut argv: Vec> = 14 | argv.iter().map(|&arg| arg.into_value().into()).collect(); 15 | unsafe { 16 | self.inner_mut() 17 | .Call( 18 | context.into(), 19 | recv.into_value().into(), 20 | argc, 21 | argv.as_mut_ptr(), 22 | ) 23 | .to_local_checked() 24 | .unwrap() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/functioncallbackinfo.rs: -------------------------------------------------------------------------------- 1 | use super::{osgood, Local, Valuable, V8}; 2 | 3 | pub struct FunctionCallbackInfo { 4 | info_: *const V8::FunctionCallbackInfo, 5 | } 6 | 7 | impl FunctionCallbackInfo { 8 | pub fn new(info_: *const V8::FunctionCallbackInfo) -> FunctionCallbackInfo { 9 | FunctionCallbackInfo { info_ } 10 | } 11 | 12 | pub fn length(&self) -> i32 { 13 | unsafe { self.info_.as_ref().unwrap().length_ } 14 | } 15 | 16 | pub fn get(&self, i: i32) -> Result, String> { 17 | if self.length() == 0 || i < 0 || i > self.length() { 18 | Err(String::from("OOB")) 19 | } else { 20 | Ok(unsafe { osgood::info_get_arg(self.info_, i).into() }) 21 | } 22 | } 23 | 24 | pub fn set_return_value(&self, ret_val: &impl Valuable) { 25 | unsafe { 26 | osgood::info_set_return_value(self.info_, ret_val.as_value().into()); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/handle_scope.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub struct HandleScope { 4 | scope_: V8::HandleScope, 5 | } 6 | 7 | impl HandleScope { 8 | pub fn new() -> HandleScope { 9 | HandleScope { 10 | scope_: unsafe { V8::HandleScope::new(Isolate::raw()) }, 11 | } 12 | } 13 | } 14 | 15 | impl Drop for HandleScope { 16 | fn drop(&mut self) { 17 | unsafe { 18 | self.scope_.destruct(); 19 | } 20 | } 21 | } 22 | 23 | impl Default for HandleScope { 24 | fn default() -> Self { 25 | Self::new() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/isolate.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::ptr; 3 | 4 | #[derive(Debug, Copy, Clone)] 5 | pub struct Isolate { 6 | isolate_: *mut V8::Isolate, 7 | } 8 | 9 | impl convert::From for *mut V8::Isolate { 10 | fn from(orig: Isolate) -> *mut V8::Isolate { 11 | orig.isolate_ 12 | } 13 | } 14 | 15 | impl Isolate { 16 | pub fn new() -> Isolate { 17 | unsafe { 18 | let params = V8::Isolate_CreateParams { 19 | code_event_handler: None, 20 | constraints: V8::ResourceConstraints { 21 | max_semi_space_size_in_kb_: 0, 22 | max_old_space_size_: 0, 23 | stack_limit_: ptr::null_mut(), 24 | code_range_size_: 0, 25 | max_zone_pool_size_: 0, 26 | }, 27 | snapshot_blob: ptr::null_mut(), 28 | counter_lookup_callback: None, 29 | create_histogram_callback: None, 30 | add_histogram_sample_callback: None, 31 | external_references: ptr::null_mut(), 32 | only_terminate_in_safe_scope: false, 33 | allow_atomics_wait: true, 34 | array_buffer_allocator: V8::ArrayBuffer_Allocator_NewDefaultAllocator(), 35 | }; 36 | let isolate = V8::Isolate::New(¶ms); 37 | V8::Isolate_SetMicrotasksPolicy(isolate, V8::MicrotasksPolicy_kAuto); 38 | Isolate::from(isolate) 39 | } 40 | } 41 | 42 | pub fn raw() -> *mut V8::Isolate { 43 | unsafe { V8::Isolate_GetCurrent() } 44 | } 45 | 46 | pub fn from(isolate_: *mut V8::Isolate) -> Isolate { 47 | Isolate { isolate_ } 48 | } 49 | 50 | pub fn enter(self) { 51 | unsafe { 52 | self.isolate_.as_mut().unwrap().Enter(); 53 | } 54 | } 55 | 56 | pub fn exit(self) { 57 | unsafe { 58 | self.isolate_.as_mut().unwrap().Exit(); 59 | } 60 | } 61 | 62 | pub fn dispose(self) { 63 | unsafe { 64 | self.isolate_.as_mut().unwrap().Dispose(); 65 | } 66 | } 67 | pub fn throw_error(error_string: &str) { 68 | unsafe { 69 | let error_string = V8::String::new_from_slice(error_string); 70 | let exception = V8::Exception_Error(error_string.into()); 71 | Isolate::raw().as_mut().unwrap().ThrowException(exception); 72 | } 73 | } 74 | 75 | pub fn throw_type_error(error_string: &str) { 76 | unsafe { 77 | let error_string = V8::String::new_from_slice(error_string); 78 | let exception = V8::Exception_TypeError(error_string.into()); 79 | Isolate::raw().as_mut().unwrap().ThrowException(exception); 80 | } 81 | } 82 | 83 | pub fn throw_range_error(error_string: &str) { 84 | unsafe { 85 | let error_string = V8::String::new_from_slice(error_string); 86 | let exception = V8::Exception_RangeError(error_string.into()); 87 | Isolate::raw().as_mut().unwrap().ThrowException(exception); 88 | } 89 | } 90 | 91 | pub fn null() -> Local { 92 | unsafe { osgood::null(Isolate::raw()).into() } 93 | } 94 | 95 | pub fn get_current_context() -> Local { 96 | unsafe { Isolate::raw().as_mut().unwrap().GetCurrentContext().into() } 97 | } 98 | } 99 | 100 | impl Default for Isolate { 101 | fn default() -> Self { 102 | Self::new() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/mod.rs: -------------------------------------------------------------------------------- 1 | use super::osgood; 2 | use super::V8; 3 | use std::convert; 4 | use std::env; 5 | use std::ffi::CString; 6 | use std::os::raw::c_char; 7 | use std::os::raw::c_int; 8 | 9 | mod local; 10 | pub use local::*; 11 | 12 | mod isolate; 13 | pub use isolate::*; 14 | 15 | mod handle_scope; 16 | pub use handle_scope::*; 17 | 18 | mod functioncallbackinfo; 19 | pub use functioncallbackinfo::*; 20 | 21 | mod script; 22 | pub use script::*; 23 | 24 | mod module; 25 | pub use module::*; 26 | 27 | mod context; 28 | pub use context::*; 29 | 30 | mod array; 31 | pub use array::*; 32 | 33 | mod object; 34 | pub use object::*; 35 | 36 | mod function; 37 | pub use function::*; 38 | 39 | mod string; 40 | pub use string::*; 41 | 42 | mod number; 43 | pub use number::*; 44 | 45 | mod array_buffer; 46 | pub use array_buffer::*; 47 | 48 | mod exception; 49 | pub use exception::*; 50 | 51 | mod private; 52 | pub use private::*; 53 | 54 | /// This is a convenience `None`, which can be used by reference as a "null" in arguments to v8 55 | /// functions. 56 | pub const NULL: Option = None; 57 | 58 | pub fn platform_init(v8_flags: &str) { 59 | let args: Vec = env::args().collect(); 60 | let name = format!("{}\0", args[0]).as_ptr() as *const c_char; 61 | let v8_flags = normalize_v8_flags(v8_flags); 62 | let flags_len = v8_flags.len() as c_int; 63 | let flags = CString::new(v8_flags).unwrap(); 64 | let flags = flags.as_ptr() as *const c_char; 65 | unsafe { 66 | osgood::platform_init(name, flags, flags_len); 67 | } 68 | } 69 | 70 | fn normalize_v8_flags(flags: &str) -> std::string::String { 71 | flags 72 | .split(' ') 73 | .filter(|x| !x.is_empty()) 74 | .map(|x| { 75 | if x.starts_with("--") { 76 | x.to_owned() 77 | } else { 78 | "--".to_owned() + x 79 | } 80 | }) 81 | .collect::>() 82 | .join(" ") 83 | } 84 | 85 | pub fn platform_dispose() { 86 | unsafe { 87 | osgood::platform_dispose(); 88 | } 89 | } 90 | 91 | pub fn process_messages() { 92 | unsafe { 93 | osgood::process_messages(Isolate::raw()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/module.rs: -------------------------------------------------------------------------------- 1 | use super::{osgood, Exception, Isolate, Local, Valuable, V8}; 2 | 3 | pub use V8::Module; 4 | 5 | enum Status { 6 | Uninstantiated, 7 | Instantiating, 8 | Instantiated, 9 | Evaluating, 10 | Evaluated, 11 | Errored, 12 | } 13 | 14 | impl Module { 15 | pub fn compile( 16 | src: Local, 17 | name: Local, 18 | ) -> Result, String> { 19 | let origin = unsafe { osgood::create_module_origin(Isolate::raw(), name.into()) }; 20 | let result = unsafe { osgood::compile_module(Isolate::raw(), origin, src.into()) }; 21 | if result.is_exception { 22 | let mut exception: Exception = result.exception.into(); 23 | Err(exception.syntax_error_stack()) 24 | } else { 25 | Ok(result.ret_val.into()) 26 | } 27 | } 28 | 29 | pub fn empty_and_throw(message: &str) -> V8::MaybeLocal { 30 | Isolate::throw_error(message); 31 | unsafe { osgood::empty_module() } 32 | } 33 | } 34 | 35 | impl Local { 36 | pub fn instantiate( 37 | &mut self, 38 | ctx: Local, 39 | callback: V8::Module_ResolveCallback, 40 | ) -> Result<(), String> { 41 | let result = unsafe { osgood::instantiate_module(ctx.into(), (*self).into(), callback) }; 42 | if result { 43 | Ok(()) 44 | } else { 45 | Err("Failed to instantiate module".to_string()) 46 | } 47 | } 48 | 49 | pub fn evaluate(&mut self, ctx: Local) -> Result, String> { 50 | let result = unsafe { osgood::evaluate_module(Isolate::raw(), ctx.into(), (*self).into()) }; 51 | if result.is_exception { 52 | Err(Local::from(result.ret_val) 53 | .to_object() 54 | .get(ctx, "stack") 55 | .as_rust_string()) 56 | } else { 57 | Ok(Local::from(result.ret_val)) 58 | } 59 | } 60 | 61 | pub fn get_hash(&mut self) -> i32 { 62 | unsafe { self.inner_mut().GetIdentityHash() } 63 | } 64 | 65 | pub fn get_exports(mut self, context: Local) -> Result, String> { 66 | unsafe { 67 | let module = self.inner_mut(); 68 | let mut exports: Local = module.GetModuleNamespace().into(); 69 | let exports = exports.inner_mut(); 70 | 71 | if !exports.IsObject() { 72 | return Err(String::from("Module namespace was not an object")); 73 | } 74 | 75 | Ok(exports.ToObject(context.into()).to_local_checked().unwrap()) 76 | } 77 | } 78 | } 79 | 80 | impl From> for V8::MaybeLocal { 81 | fn from(wrapped: Local) -> V8::MaybeLocal { 82 | unsafe { osgood::from_local_module(wrapped.into()) } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/number.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub use V8::Number; 4 | 5 | impl V8::Number { 6 | pub fn new(i: f64) -> Local { 7 | unsafe { V8::Number_New(Isolate::raw(), i).into() } 8 | } 9 | } 10 | 11 | impl Local { 12 | pub fn value(&mut self) -> f64 { 13 | unsafe { self.inner_mut().Value() } 14 | } 15 | } 16 | 17 | impl IntoValue for f64 { 18 | fn into_value(&self) -> Local { 19 | V8::Number::new(*self).into() 20 | } 21 | } 22 | 23 | impl IntoValue for i32 { 24 | fn into_value(&self) -> Local { 25 | V8::Number::new(f64::from(*self)).into() 26 | } 27 | } 28 | 29 | impl IntoValue for u16 { 30 | fn into_value(&self) -> Local { 31 | V8::Number::new(f64::from(*self)).into() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/object.rs: -------------------------------------------------------------------------------- 1 | use super::{osgood, Context, IntoValue, Isolate, Local, Valuable, V8}; 2 | 3 | pub use V8::Object; 4 | use V8::Value; 5 | 6 | type FunctionCallback = unsafe extern "C" fn(info: *const V8::FunctionCallbackInfo); 7 | 8 | impl Object { 9 | pub fn new() -> Local { 10 | unsafe { V8::Object_New(Isolate::raw()).into() } 11 | } 12 | } 13 | 14 | impl Local { 15 | pub fn set_extern_method( 16 | &mut self, 17 | context: Local, 18 | name: &str, 19 | func: FunctionCallback, 20 | ) { 21 | unsafe { 22 | let v8_name = V8::String::new_from_slice(name).as_value(); 23 | let tmpl = osgood::new_function_template(Isolate::raw(), Some(func)); 24 | let v8_fn = tmpl 25 | .val_ 26 | .as_mut() 27 | .unwrap() 28 | .GetFunction(context.into()) 29 | .to_local_checked() 30 | .unwrap() 31 | .into(); 32 | let v8_fn_val = 33 | std::mem::transmute::, V8::Local>(v8_fn); 34 | self.inner_mut().Set(v8_name.into(), v8_fn_val); 35 | } 36 | } 37 | 38 | pub fn set(&mut self, name: &str, val: impl IntoValue) { 39 | unsafe { 40 | let key = V8::String::new_from_slice(name); 41 | self.inner_mut() 42 | .Set(key.as_value().into(), val.into_value().into()); 43 | } 44 | } 45 | 46 | // TODO optimize so that they key can be a value 47 | pub fn get(&mut self, context: Local, name: &str) -> Local { 48 | unsafe { 49 | let key = V8::String::new_from_slice(name); 50 | self.inner_mut() 51 | .Get1(context.into(), key.as_value().into()) 52 | .to_local_checked() 53 | .unwrap() 54 | } 55 | } 56 | 57 | pub fn set_private(&mut self, context: Local, key_name: &str, val: impl IntoValue) { 58 | unsafe { 59 | let priv_key = private(key_name); 60 | self.inner_mut() 61 | .SetPrivate(context.into(), priv_key, val.into_value().into()); 62 | } 63 | } 64 | 65 | pub fn get_private(&mut self, context: Local, key_name: &str) -> Local { 66 | unsafe { 67 | let priv_key = private(key_name); 68 | self.inner_mut() 69 | .GetPrivate(context.into(), priv_key) 70 | .to_local_checked() 71 | .unwrap() 72 | } 73 | } 74 | 75 | pub fn iter(self, context: Local) -> ObjectIterator { 76 | ObjectIterator::new(self, context) 77 | } 78 | } 79 | 80 | pub struct ObjectIterator { 81 | obj: Local, 82 | len: usize, 83 | cursor: usize, 84 | names: Local, 85 | context: Local, 86 | } 87 | 88 | impl ObjectIterator { 89 | fn new(mut obj: Local, context: Local) -> ObjectIterator { 90 | let mut names: Local = unsafe { 91 | obj.inner_mut() 92 | .GetOwnPropertyNames(context.into()) 93 | .to_local_checked() 94 | .unwrap() 95 | }; 96 | let length = names.length() as usize; 97 | 98 | ObjectIterator { 99 | obj, 100 | len: length, 101 | cursor: 0, 102 | names: names.as_value().to_object(), 103 | context, 104 | } 105 | } 106 | } 107 | 108 | impl std::iter::Iterator for ObjectIterator { 109 | type Item = (Local, Local); 110 | fn next(&mut self) -> Option { 111 | if self.cursor == self.len { 112 | None 113 | } else { 114 | let name = self.names.get(self.context, &self.cursor.to_string()); 115 | let val = unsafe { self.obj.inner_mut().Get(name.into()) }; 116 | self.cursor += 1; 117 | Some((name, val.into())) 118 | } 119 | } 120 | } 121 | 122 | unsafe fn private(key_name: &str) -> V8::Local { 123 | let isolate = Isolate::raw(); 124 | V8::Private::ForApi(isolate, V8::String::new_from_slice(key_name).into()) 125 | } 126 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/private.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub use V8::Private; 4 | 5 | impl Private { 6 | pub fn for_api(name: &str) -> Local { 7 | unsafe { 8 | V8::Private_ForApi(Isolate::raw(), V8::String::new_from_slice(name).into()).into() 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /osgood-v8/src/wrapper/script.rs: -------------------------------------------------------------------------------- 1 | use super::{osgood, Exception, Isolate, Local, Valuable, V8}; 2 | 3 | pub use V8::Script; 4 | 5 | impl Script { 6 | pub fn compile( 7 | ctx: Local, 8 | src: Local, 9 | ) -> Result, std::string::String> { 10 | unsafe { 11 | let result = osgood::compile_script(Isolate::raw(), ctx.into(), src.into()); 12 | if result.is_exception { 13 | let mut exception: Exception = result.exception.into(); 14 | Err(exception.syntax_error_stack()) 15 | } else { 16 | Ok(Local::from(result.ret_val)) 17 | } 18 | } 19 | } 20 | } 21 | 22 | impl Local