├── .dockerignore ├── .gitignore ├── .homebase.yml ├── Dockerfile ├── LICENSE ├── README.md ├── grafana-dashboard.json ├── grafana-screenshot.png ├── index.js ├── lib ├── config.js ├── errors.js ├── hyperdrive-directory-listing-page.js ├── index.js ├── metrics.js ├── mime.js ├── server.js └── vhosts │ ├── hyperdrive.js │ ├── proxy.js │ └── redirect.js ├── package.json └── test ├── config-test.js ├── scaffold ├── empty.yml ├── everything-disabled.yml └── full.yml └── util.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.homebase.yml: -------------------------------------------------------------------------------- 1 | directory: ~/.homebase # where your data will be stored 2 | httpMirror: true # enables http mirrors of the dats 3 | ports: 4 | http: 80 # HTTP port for serving 5 | #letsencrypt: # set to false to disable lets-encrypt 6 | # email: bob@foo.com # you must provide your email to LE for admin 7 | # agreeTos: true # you must agree to the LE terms (set to true) 8 | 9 | # Note: exposes a web-page that's available *without* authentication and has your hyper and dat urls on it. 10 | #dashboard: # set to false to disable 11 | # port: 8089 # port for accessing the metrics dashboard 12 | 13 | # enter your pinned hyperdrives here 14 | hyperdrives: [] # `[]` for empty list. Remove the `[]` if you have hyperdrives listed 15 | #- url: hyper://1f968afe867f06b0d344c11efc23591c7f8c5fb3b4ac938d6000f330f6ee2a03/ 16 | # domains: 17 | # - mysite.com 18 | # - my-site.com 19 | #- url: 868d6000f330f6967f06b3ee2a03811efc23591afe0d344cc7f8c5fb3b4ac91f 20 | # domain: 21 | # - othersite.com 22 | 23 | # enter any proxied routes here 24 | proxies: [] # `[]` for empty list. Remove the `[]` if you have proxies listed 25 | #- from: myproxy.com 26 | # to: https://mysite.com/ 27 | #- from: foo.proxy.edu 28 | # to: http://localhost:8080/ 29 | #- from: best-proxy-ever 30 | # to: http://127.0.0.1:123/ 31 | 32 | # enter any redirect routes here 33 | redirects: [] # `[]` for empty list. Remove the `[]` if you have proxies listed 34 | #- from: myredirect.com 35 | # to: https://mysite.com/ 36 | #- from: foo.redirect.edu 37 | # to: http://localhost:8080/ 38 | #- from: best-redirect-ever 39 | # to: http://127.0.0.1:123/ 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Credits: https://nodejs.org/en/docs/guides/nodejs-docker-webapp/ 2 | FROM node:8-stretch 3 | 4 | RUN apt-get update && apt-get install -y libtool m4 automake libcap2-bin build-essential 5 | 6 | # Create app directory 7 | WORKDIR /usr/src/app 8 | 9 | # Install app dependencies 10 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 11 | # where available (npm@5+) 12 | COPY package*.json ./ 13 | 14 | # If you are building your code for production 15 | RUN npm install --only=production 16 | RUN npm install pm2 -g 17 | 18 | COPY . . 19 | 20 | COPY .homebase.yml /root/.homebase.yml 21 | 22 | # From Node's Best Practices 23 | # See: https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#environment-variables 24 | # Note: Only this is applied from the practices. 25 | ENV NODE_ENV production 26 | 27 | # Note: you still need to supply -p 80:80 -p 443:443 -p 3282:3282 -p 8089:8089 28 | # If you have Beaker opened, you may want to change the 3282 port. E.g., to run with -p 9999:3282 29 | EXPOSE 80 443 3282 8089 30 | 31 | CMD [ "pm2-runtime", "npm", "--", "start" ] 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Blue Link Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebase 2 | 3 | `homebase` is a self-deployable tool for managing websites published with the [Hypercore protocol](https://hypercore-protocol.org/). 4 | 5 | `homebase` is for you if: 6 | 7 | - You're comfortable with some server administration (or want to learn!) 8 | - You want to keep your hyper:// website/s online 9 | - You want to publish your hyper:// website/s to http:// 10 | 11 | ## Table of contents 12 | 13 | - [Install](#install) 14 | - [Running homebase](#running-homebase) 15 | - [Examples](#examples) 16 | - [Configuration](#configuration) 17 | - [Advanced examples](#advanced-examples) 18 | - [Troubleshooting](#troubleshooting) 19 | - [Support](#support) 20 | - [Web API Clients](#web-api-clients) 21 | - [Changelog](#changelog) 22 | 23 | ## Install 24 | 25 | If you already have [Node.js](https://nodejs.org) (12.0+) and [npm](https://npmjs.com) installed on your server, get started by installing Homebase with npm or [npx](https://github.com/zkat/npx). 26 | 27 | ```bash 28 | npm install -g @beaker/homebase 29 | ``` 30 | 31 | Otherwise, install Node.js and npm first: 32 | 33 | - [Install Node.js](https://nodejs.org/en/download/) 34 | - [nvm](https://github.com/creationix/nvm) for managing Node versions 35 | - [Fixing npm permissions problems](https://docs.npmjs.com/getting-started/fixing-npm-permissions) 36 | 37 | Having trouble installing? See [Troubleshooting](#troubleshooting). 38 | 39 | ## Running homebase 40 | 41 | To run `homebase` manually, simply invoke the `homebase` command: 42 | 43 | ```bash 44 | homebase 45 | ``` 46 | 47 | To keep `homebase` running, you'll need to daemonize it. We like using [pm2](https://www.npmjs.com/package/pm2). 48 | 49 | ```bash 50 | # install pm2 51 | npm i -g pm2 52 | 53 | # start homebase with pm2 54 | pm2 start homebase 55 | ``` 56 | 57 | To stop the daemon, run: 58 | 59 | ``` 60 | # stop homebase 61 | pm2 stop homebase 62 | ``` 63 | 64 | ### Command line flags 65 | 66 | - `--config ` 67 | - Use the config file at the given path instead of the default `~/.homebase.yml`. Overrides the value of the HOMEBASE_CONFIG env var. 68 | 69 | ### Environment variables 70 | 71 | - `HOMEBASE_CONFIG=cfg_file_path` 72 | - Specify an alternative path to the config than `~/.homebase.yml` 73 | - `NODE_ENV=debug|staging|production` 74 | - Set to debug or staging to use the Let's Encrypt testing servers. 75 | 76 | ## Examples 77 | 78 | `homebase` uses a [configuration file](#configuration-file) (`~/.homebase.yml` by default) for managing its behavior. These examples show various configurations. 79 | 80 | [See all configuration options](#configuration) 81 | 82 | ### Example: set up a hyperdrive with HTTP mirroring 83 | 84 | This configuration file will host the files at `hyper://123...456` and mirror those files to `http://alice.com`. 85 | 86 | This example uses a domain name, so in order for the domain name to resolve correctly, you'll need to update your DNS configuration first. In this case, you could set an `A` record that points to the `homebase` server's IP address. 87 | 88 | ```yaml 89 | hyperdrives: 90 | - url: hyper://123...456 91 | domains: 92 | - alice.com 93 | httpMirror: true 94 | ``` 95 | 96 | ### Example: host multiple websites, with no HTTP mirroring 97 | 98 | This configuration simply hosts the files at `hyper://123...456` and `hyper:///456...789`. No domain name is required for this configuration. 99 | 100 | ```yaml 101 | hyperdrives: 102 | - url: hyper://123...456 103 | - url: hyper://456...789 104 | ``` 105 | 106 | ## Configuration 107 | 108 | - [Configuration file](#configuration-file) 109 | - [dashboard](#dashboard) 110 | - [dashboard.port](#dashboardport) 111 | - [hyperdrives](#hyperdrives) 112 | - [hyperdrives.*.url](#hyperdrivesurl) 113 | - [hyperdrives.*.domains](#hyperdrivesdomains) 114 | - [directory](#directory) 115 | - [domain](#domain) 116 | - [httpMirror](#httpmirror) 117 | - [ports](#ports) 118 | - [ports.http](#portshttp) 119 | - [proxies](#proxies) 120 | - [proxies.*.from](#proxiesfrom) 121 | - [proxies.*.to](#proxiesto) 122 | - [redirects](#redirects) 123 | - [redirects.*.from](#redirectsfrom) 124 | - [redirects.*.to](#redirectsto) 125 | 126 | ### Configuration file 127 | 128 | `homebase` uses `~/.homebase.yml` as its default configuration file. You can specify an alternative config file using a [command line flag](#command-line-flags) or an [environment variable](#environment-variables). 129 | 130 | 131 | ```yaml 132 | directory: ~/.homebase # where your data will be stored 133 | httpMirror: true # enables HTTP mirroring 134 | ports: 135 | http: 80 # HTTP port for redirects or non-TLS serving 136 | dashboard: # set to false to disable 137 | port: 8089 # port for accessing the metrics dashboard 138 | 139 | # enter your hosted hyperdrives here 140 | hyperdrives: 141 | - url: # URL of the hyperdrive to be hosted 142 | domains: # (optional) the domains of the hyperdrive 143 | 144 | # enter any proxied routes here 145 | proxies: 146 | - from: # the domain to accept requests from 147 | to: # the domain (& port) to target 148 | 149 | # enter any redirect routes here 150 | redirects: 151 | - from: # the domain to accept requests from 152 | to: # the domain to redirect to 153 | ``` 154 | 155 | ### dashboard 156 | 157 | Default `false` 158 | 159 | Set to `true` to enable the [Prometheus metrics dashboard](#example-using-a-metrics-dashboard). 160 | 161 | ```yaml 162 | dashboard: # set to false to disable 163 | port: 8089 # port for accessing the metrics dashboard 164 | ``` 165 | 166 | #### dashboard.port 167 | 168 | Default: `8089` 169 | 170 | The port to serve the [Prometheus metrics dashboard](#example-using-a-metrics-dashboard). 171 | 172 | ### hyperdrives 173 | 174 | A listing of the Hyperdrives to host. 175 | 176 | ```yaml 177 | hyperdrives: 178 | - url: hyper://1f968afe867f06b0d344c11efc23591c7f8c5fb3b4ac938d6000f330f6ee2a03/ 179 | domains: 180 | - mysite.com 181 | - my-site.com 182 | ``` 183 | 184 | #### hyperdrives.*.url 185 | 186 | The Hyperdrive URL of the site to host. Should be a 'raw' hyper url (no DNS hostname). 187 | 188 | Example values: 189 | 190 | ``` 191 | # raw key 192 | 1f968afe867f06b0d344c11efc23591c7f8c5fb3b4ac938d6000f330f6ee2a03 193 | 194 | # URL with trailing slash 195 | hyper://1f968afe867f06b0d344c11efc23591c7f8c5fb3b4ac938d6000f330f6ee2a03/ 196 | 197 | # URL with no trailing slash 198 | hyper://1f968afe867f06b0d344c11efc23591c7f8c5fb3b4ac938d6000f330f6ee2a03 199 | ``` 200 | 201 | #### hyperdrives.*.domains 202 | 203 | Additional domains of the Hyperdrive. Can be a string or a list of strings. Each string should be a domain name. 204 | 205 | To use `hyperdrives.*.domains`, you'll first need to configure the DNS entry for your domain name to point to your server. For instance, to point `alice.com` with `homebase`, you'll need to update your DNS configuration to point `alice.com` to your homebase server's IP address. 206 | 207 | Example values: 208 | 209 | ``` 210 | mysite.com 211 | foo.bar.edu 212 | best-site-ever.link 213 | ``` 214 | 215 | ### directory 216 | 217 | Default: `~/.homebase` 218 | 219 | The directory where `homebase` will store your data. 220 | 221 | ### domain 222 | 223 | **DEPRECATED**. See the [v2.0.0 migration guide](#migrating-to-v2-0-0). 224 | 225 | The DNS domain of your homebase instance. 226 | 227 | ### httpMirror 228 | 229 | Default: `false` 230 | 231 | Set to `true` to provide http mirroring of your Hyperdrives. 232 | 233 | ### ports 234 | 235 | The ports for HTTP. 236 | 237 | ```yaml 238 | ports: 239 | http: 80 240 | ``` 241 | 242 | #### ports.http 243 | 244 | Default: `80` 245 | 246 | The port for serving HTTP sites. 247 | 248 | ### proxies 249 | 250 | A listing of domains to proxy. Useful when your server has other services running that you need available. 251 | 252 | ```yaml 253 | proxies: 254 | - from: my-proxy.com 255 | to: http://localhost:8080 256 | ``` 257 | 258 | #### proxies.*.from 259 | 260 | The domain to proxy from. Should be a domain name. 261 | 262 | Example values: 263 | 264 | ``` 265 | mysite.com 266 | foo.bar.edu 267 | best-site-ever.link 268 | ``` 269 | 270 | #### proxies.*.to 271 | 272 | The protocol, domain, and port to proxy to. Should be an [origin](https://en.wikipedia.org/wiki/Same-origin_policy#Origin_determination_rules). 273 | 274 | Example values: 275 | 276 | ``` 277 | https://mysite.com/ 278 | http://localhost:8080/ 279 | http://127.0.0.1:123/ 280 | ``` 281 | 282 | ### redirects 283 | 284 | A listing of domains to redirect. 285 | 286 | ```yaml 287 | redirects: 288 | - from: my-old-site.com 289 | to: https://my-site.com 290 | ``` 291 | 292 | #### redirects.*.from 293 | 294 | The domain to redirect from. Should be a domain name. 295 | 296 | Example values: 297 | 298 | ``` 299 | mysite.com 300 | foo.bar.edu 301 | best-site-ever.link 302 | ``` 303 | 304 | #### redirects.*.to 305 | 306 | The base URL to redirect to. Should be an [origin](https://en.wikipedia.org/wiki/Same-origin_policy#Origin_determination_rules). 307 | 308 | Example values: 309 | 310 | ``` 311 | https://mysite.com/ 312 | http://localhost:8080/ 313 | http://127.0.0.1:123/ 314 | ``` 315 | 316 | ## Advanced examples 317 | 318 | ### Example: proxies 319 | 320 | If your `homebase` instance is running on ports 80/443, and you have other Web servers running on your server, you might need `homebase` to proxy to those other servers. You can do that with the `proxies` config. Here's an example proxy rule: 321 | 322 | [See full `proxies` reference](#proxies) 323 | 324 | ```yaml 325 | proxies: 326 | - from: my-proxy.com 327 | to: http://localhost:8080 328 | ``` 329 | 330 | ### Example: redirecting requests 331 | 332 | Sometimes you need to redirect old domains to new ones. You can do that with the `redirects` rule. Here's an example redirect rule: 333 | 334 | [See full `redirects` reference](#redirects) 335 | 336 | ```yaml 337 | redirects: 338 | - from: my-old-site.com 339 | to: https://my-site.com 340 | ``` 341 | 342 | ### Example: using a metrics dashboard 343 | 344 | `homebase` has built-in support for [Prometheus](https://prometheus.io), which can be visualized with [Grafana](http://grafana.org/). 345 | 346 | ![./grafana-screenshot.png](./grafana-screenshot.png) 347 | 348 | Homebase exposes its metrics at port 8089. Prometheus periodically scrapes the metrics and stores them in a database. Grafana uses those metrics and provides a provides a nice dashboard visualization. It's a little daunting at first, but setup should be relatively painless. 349 | 350 | Steps: 351 | 352 | 1. [Install Prometheus](https://prometheus.io/download/) on your server 353 | 2. [Install Grafana](http://grafana.org/download/) on your server 354 | 3. Update the `prometheus.yml` config 355 | 4. Start Prometheus and Grafana 356 | 5. Login to Grafana 357 | 6. Add Prometheus as a data source to Grafana (it should be running at `localhost:9090` 358 | 7. Import [this Grafana dashboard](./grafana-dashboard.json) 359 | 360 | Your `prometheus.yml` config should include have the `scrape_configs` option set like this: 361 | 362 | ```yaml 363 | scrape_configs: 364 | - job_name: 'prometheus' 365 | static_configs: 366 | - targets: ['localhost:9090'] 367 | - job_name: 'homebase' 368 | static_configs: 369 | - targets: ['localhost:8089'] 370 | ``` 371 | 372 | ### Example: running homebase behind Apache or Nginx 373 | 374 | If you're running `homebase` on a server that uses Apache or Nginx, you may need to change your config to disable HTTPS. For instance, if you're using nginx and proxying to port `8080`, update your config to set the HTTP port: 375 | 376 | ```yaml 377 | ports: 378 | http: 8080 379 | ``` 380 | 381 | You will need to add all domains to your Nginx/Apache config. 382 | 383 | ### Example: running homebase in a docker container 384 | 385 | 1. Install [Docker](http://docker.com/). If you're on Linux, remember to [configure Docker to start on boot](https://docs.docker.com/install/linux/linux-postinstall/). Don't know of the equivalent for other systems. 386 | 387 | 2. Clone the project. Edit `.homebase.yml` according to your needs. Most importantly: **Change username and password**. 388 | If you don't want to think of a username and a password, just use [this](https://stackoverflow.com/a/1349426/6690391) but **increase the length**. 389 | 390 | 3. In the project root, run this command: 391 | 392 | ``` 393 | docker build -t homebase:latest . && docker run -d --name=homebase --restart=always -p 80:80 -p 443:443 -p 3282:3282 homebase:latest 394 | ``` 395 | 396 | **Notes:** 397 | 1. Not an expert in Docker security or configuration. 398 | 2. if you have Beaker on the same machine, you may want to change the hyperdrive port `-p 3282:3282` to something like `-p 9999:3282`. 399 | 3. To debug the running container: 400 | - Run `docker ps -a` to see the container running status. 401 | - Run `docker logs homebase` to see the logs. 402 | - Run `docker exec -it homebase sh` to get into a terminal. 403 | 4. Didn't think about how you'd install a newer version of homebase while keeping the old configuration and data. 404 | 405 | ## Troubleshooting 406 | 407 | ### Installing build dependencies 408 | 409 | When installing `homebase`, you may need to install additional build dependencies: 410 | 411 | ``` 412 | sudo apt-get install libtool m4 automake libcap2-bin build-essential 413 | ``` 414 | 415 | ### Port setup (EACCES error) 416 | 417 | For `homebase` to work correctly, you need to be able to access port 80 (http), 443 (https), and 3282 (hyperdrive). Your firewall should be configured to allow traffic on those ports. 418 | 419 | If you get an EACCES error on startup, you either have a process using the port already, or you lack permission to use the port. Try `lsof -i tcp:80` or `lsof -i tcp:443` to see if there are any processes bound to the ports you need. 420 | 421 | If the ports are not in use, then it's probably a permissions problem. We recommend using the following command to solve that: 422 | 423 | ``` 424 | # give node perms to use ports 80 and 443 425 | sudo setcap cap_net_bind_service=+ep `readlink -f \`which node\`` 426 | ``` 427 | 428 | This will give nodejs the rights to use ports 80 and 443. This is preferable to running homebase as root, because that carries some risk of a bug in `homebase` allowing somebody to control your server. 429 | 430 | ## Support 431 | 432 | `homebase` is built by the [Beaker Browser team](https://beakerbrowser.com/about). Become a backer and help support the development of an open, friendly, and fun Web. You can help us continue our work on Beaker, [hashbase.io](https://hashbase.io), `homebase`, and more. Thank you! 433 | 434 | [Become a backer](https://opencollective.com/beaker) 435 | 436 | 437 | 438 | 439 | ## Changelog 440 | 441 | ### v3.0.0 442 | 443 | - Added Hyperdrive support. 444 | - Deprecated Dat support. If you still need dat support, you'll need to use Homebase v2. 445 | - Deprecated the Web API. 446 | - Deprecated Lets Encrypt due to Greenlock changing too much to keep up with. 447 | 448 | ### v2.0.0 449 | 450 | - Removed the `dats.*.name` field. You can now set the domains for your dats directly with the `dat.*.domains` field. 451 | - Moved the `domain` config from the top of the yaml file to the `webapi` field. This makes it clearer what the domain applies to. Optional, unless you want to use Let's Encrypt. 452 | 453 | The original release of `homebase` tried to mimic [Hashbase](https://github.com/beakerbrowser/hashbase) as closely as possible. As a result, it had a concept of a root domain and each dat was given a `name` which became a subdomain under that root domain. This confused most users and was generally regarded as "the worst." To simplify the config process, we removed the concept of the root domain and `name` attribute. Now, you just set the domains directly on each dat. 454 | 455 | -------------------------------------------------------------------------------- /grafana-dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "grafana", 15 | "id": "grafana", 16 | "name": "Grafana", 17 | "version": "4.1.2" 18 | }, 19 | { 20 | "type": "panel", 21 | "id": "graph", 22 | "name": "Graph", 23 | "version": "" 24 | }, 25 | { 26 | "type": "datasource", 27 | "id": "prometheus", 28 | "name": "Prometheus", 29 | "version": "1.0.0" 30 | } 31 | ], 32 | "annotations": { 33 | "list": [] 34 | }, 35 | "editable": true, 36 | "gnetId": null, 37 | "graphTooltip": 0, 38 | "hideControls": false, 39 | "id": null, 40 | "links": [], 41 | "refresh": false, 42 | "rows": [ 43 | { 44 | "collapse": false, 45 | "height": "250px", 46 | "panels": [ 47 | { 48 | "aliasColors": {}, 49 | "bars": false, 50 | "datasource": "${DS_PROMETHEUS}", 51 | "decimals": 0, 52 | "fill": 0, 53 | "id": 1, 54 | "legend": { 55 | "alignAsTable": true, 56 | "avg": false, 57 | "current": true, 58 | "hideZero": false, 59 | "max": true, 60 | "min": false, 61 | "rightSide": true, 62 | "show": true, 63 | "total": false, 64 | "values": true 65 | }, 66 | "lines": true, 67 | "linewidth": 1, 68 | "links": [], 69 | "nullPointMode": "null", 70 | "percentage": false, 71 | "pointradius": 5, 72 | "points": false, 73 | "renderer": "flot", 74 | "seriesOverrides": [], 75 | "span": 12, 76 | "stack": false, 77 | "steppedLine": true, 78 | "targets": [ 79 | { 80 | "expr": "app_dat_peers", 81 | "intervalFactor": 2, 82 | "legendFormat": "{{dat}}", 83 | "metric": "app_dat_peers", 84 | "refId": "A", 85 | "step": 4 86 | } 87 | ], 88 | "thresholds": [], 89 | "timeFrom": null, 90 | "timeShift": null, 91 | "title": "Number of Connections", 92 | "tooltip": { 93 | "shared": true, 94 | "sort": 0, 95 | "value_type": "individual" 96 | }, 97 | "transparent": false, 98 | "type": "graph", 99 | "xaxis": { 100 | "mode": "time", 101 | "name": null, 102 | "show": true, 103 | "values": [] 104 | }, 105 | "yaxes": [ 106 | { 107 | "format": "none", 108 | "label": null, 109 | "logBase": 1, 110 | "max": null, 111 | "min": "0", 112 | "show": true 113 | }, 114 | { 115 | "format": "short", 116 | "label": null, 117 | "logBase": 1, 118 | "max": null, 119 | "min": null, 120 | "show": false 121 | } 122 | ] 123 | } 124 | ], 125 | "repeat": null, 126 | "repeatIteration": null, 127 | "repeatRowId": null, 128 | "showTitle": false, 129 | "title": "Dashboard Row", 130 | "titleSize": "h6" 131 | }, 132 | { 133 | "collapse": false, 134 | "height": 250, 135 | "panels": [ 136 | { 137 | "aliasColors": {}, 138 | "bars": false, 139 | "datasource": "${DS_PROMETHEUS}", 140 | "decimals": 0, 141 | "fill": 0, 142 | "id": 2, 143 | "legend": { 144 | "alignAsTable": true, 145 | "avg": false, 146 | "current": true, 147 | "hideEmpty": true, 148 | "hideZero": true, 149 | "max": false, 150 | "min": false, 151 | "rightSide": true, 152 | "show": true, 153 | "sort": "current", 154 | "sortDesc": true, 155 | "total": false, 156 | "values": true 157 | }, 158 | "lines": true, 159 | "linewidth": 1, 160 | "links": [], 161 | "nullPointMode": "null", 162 | "percentage": false, 163 | "pointradius": 1, 164 | "points": false, 165 | "renderer": "flot", 166 | "seriesOverrides": [ 167 | {} 168 | ], 169 | "span": 12, 170 | "stack": false, 171 | "steppedLine": false, 172 | "targets": [ 173 | { 174 | "expr": "app_https_hits", 175 | "hide": false, 176 | "interval": "", 177 | "intervalFactor": 2, 178 | "legendFormat": "{{hostname}}{{path}}", 179 | "metric": "app_https_hits", 180 | "refId": "A", 181 | "step": 4 182 | } 183 | ], 184 | "thresholds": [], 185 | "timeFrom": null, 186 | "timeShift": null, 187 | "title": "HTTPS Page Hits", 188 | "tooltip": { 189 | "shared": true, 190 | "sort": 0, 191 | "value_type": "individual" 192 | }, 193 | "type": "graph", 194 | "xaxis": { 195 | "mode": "time", 196 | "name": null, 197 | "show": true, 198 | "values": [] 199 | }, 200 | "yaxes": [ 201 | { 202 | "format": "none", 203 | "label": null, 204 | "logBase": 1, 205 | "max": null, 206 | "min": "0", 207 | "show": true 208 | }, 209 | { 210 | "format": "short", 211 | "label": null, 212 | "logBase": 1, 213 | "max": null, 214 | "min": null, 215 | "show": false 216 | } 217 | ] 218 | } 219 | ], 220 | "repeat": null, 221 | "repeatIteration": null, 222 | "repeatRowId": null, 223 | "showTitle": false, 224 | "title": "Dashboard Row", 225 | "titleSize": "h6" 226 | } 227 | ], 228 | "schemaVersion": 14, 229 | "style": "dark", 230 | "tags": [], 231 | "templating": { 232 | "list": [] 233 | }, 234 | "time": { 235 | "from": "now-1h", 236 | "to": "now" 237 | }, 238 | "timepicker": { 239 | "refresh_intervals": [ 240 | "5s", 241 | "10s", 242 | "30s", 243 | "1m", 244 | "5m", 245 | "15m", 246 | "30m", 247 | "1h", 248 | "2h", 249 | "1d" 250 | ], 251 | "time_options": [ 252 | "5m", 253 | "15m", 254 | "1h", 255 | "6h", 256 | "12h", 257 | "24h", 258 | "2d", 259 | "7d", 260 | "30d" 261 | ] 262 | }, 263 | "timezone": "browser", 264 | "title": "DatHTTPD", 265 | "version": 4 266 | } -------------------------------------------------------------------------------- /grafana-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/homebase/706d94ffe80e9f88b52f2419be8cf1fb3eaf3546/grafana-screenshot.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const os = require('os') 3 | const path = require('path') 4 | const {HomebaseConfig} = require('./lib/config') 5 | const server = require('./lib/server') 6 | 7 | const defaultConfigPath = process.env.HOMEBASE_CONFIG || path.join(os.homedir(), '.homebase.yml') 8 | 9 | const argv = require('yargs') 10 | .usage('homebase - Start a homebase server') 11 | .option('config', { 12 | describe: 'Path to the config file. If no path is given, the path to the config is looked up in the HOMEBASE_CONFIG environment variable. If this is not set, the config will be read from the default path ~/.homebase.yml.', 13 | default: defaultConfigPath 14 | }) 15 | .argv 16 | 17 | // read config and start the server 18 | var config = new HomebaseConfig(argv.config) 19 | server.start(config) 20 | 21 | process.on('uncaughtException', console.error) 22 | process.on('unhandledRejection', console.error) 23 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Homebase config 4 | 5 | The main source of truth for homebase's config is the yaml file. 6 | The user can change that yaml during operation and homebase will reload it and run the changes. 7 | 8 | The user can also modify the active config using web apis. 9 | In that case, we need to serialize the change back into the yaml file. 10 | So that we change as little as possible, we maintain the values as given by the user ('canonical'). 11 | That way, when we write back to the yaml file, we don't write back a bunch of defaults. 12 | 13 | The structure of this is the HomebaseConfig classes which wrap canonical. 14 | They use getters to provide computed info and defaults. 15 | 16 | NOTE: we should *not* modify config to avoid writing those edits back to the yaml! 17 | 18 | */ 19 | 20 | const os = require('os') 21 | const path = require('path') 22 | const fs = require('fs') 23 | const EventEmitter = require('events') 24 | const yaml = require('js-yaml') 25 | const untildify = require('untildify') 26 | const isDomain = require('is-domain-name') 27 | const isOrigin = require('is-http-url') 28 | const _flatten = require('lodash.flatten') 29 | const {ConfigError} = require('./errors') 30 | 31 | const DEFAULT_CONFIG_DIRECTORY = path.join(os.homedir(), '.homebase') 32 | const IS_DEBUG = (['debug', 'staging', 'test'].indexOf(process.env.NODE_ENV) !== -1) 33 | 34 | // exported api 35 | // = 36 | 37 | class HomebaseConfig { 38 | constructor (configPath = false) { 39 | this.events = new EventEmitter() 40 | 41 | // where the config is loaded from 42 | this.configPath = null 43 | 44 | // `canonical` the canonical config 45 | // - reflects *only* the values that users set 46 | // - first read from the yaml file 47 | // - can then be updated by APIs 48 | // - *may* be slightly massaged so long as it won't annoy users 49 | this.canonical = {} 50 | 51 | if (configPath) { 52 | this.readFromFile(configPath) 53 | } 54 | } 55 | 56 | readFromFile (configPath = false) { 57 | configPath = configPath || this.configPath 58 | this.configPath = configPath 59 | var configContents 60 | 61 | // read file 62 | try { 63 | configContents = fs.readFileSync(configPath, 'utf8') 64 | } catch (e) { 65 | // throw if other than a not-found 66 | configContents = '' 67 | if (e.code !== 'ENOENT') { 68 | console.error('Failed to load config file at', configPath) 69 | throw e 70 | } 71 | } 72 | 73 | // parse 74 | try { 75 | this.canonical = yaml.safeLoad(configContents) 76 | } catch (e) { 77 | console.error('Failed to parse config file at', configPath) 78 | throw e 79 | } 80 | this.canonical = this.canonical || {} 81 | 82 | // validate 83 | validate(this.canonical) 84 | 85 | this.events.emit('read-config') 86 | } 87 | 88 | get directory () { 89 | return untildify(this.canonical.directory || DEFAULT_CONFIG_DIRECTORY) 90 | } 91 | 92 | get hyperspaceDirectory () { 93 | return path.join(this.directory, 'hyperspace') 94 | } 95 | 96 | get httpMirror () { 97 | return this.canonical.httpMirror || false 98 | } 99 | 100 | get ports () { 101 | var ports = this.canonical.ports || {} 102 | ports.http = ports.http || 80 103 | return ports 104 | } 105 | 106 | get dashboard () { 107 | return this.canonical.dashboard || false 108 | } 109 | 110 | get hyperdrives () { 111 | return this.canonical.hyperdrives ? this.canonical.hyperdrives.map(v => new HomebaseHyperdriveConfig(v, this)) : [] 112 | } 113 | 114 | get proxies () { 115 | return this.canonical.proxies ? this.canonical.proxies.map(v => new HomebaseProxyConfig(v, this)) : [] 116 | } 117 | 118 | get redirects () { 119 | return this.canonical.redirects ? this.canonical.redirects.map(v => new HomebaseRedirectConfig(v, this)) : [] 120 | } 121 | 122 | get allVhosts () { 123 | return this.hyperdrives.concat(this.proxies).concat(this.redirects) 124 | } 125 | 126 | get hostnames () { 127 | return _flatten(this.allVhosts.map(vhostCfg => vhostCfg.hostnames)).filter(Boolean) 128 | } 129 | } 130 | 131 | class HomebaseHyperdriveConfig { 132 | constructor (canonical, config) { 133 | for (var k in canonical) { 134 | this[k] = canonical[k] 135 | } 136 | this.config = config 137 | } 138 | 139 | get id () { 140 | return 'hyperdrive-' + this.hyperdriveKey 141 | } 142 | 143 | get vhostType () { 144 | return 'hyperdrive' 145 | } 146 | 147 | get hyperdriveKey () { 148 | return getHyperdriveKey(this.url) 149 | } 150 | 151 | get hostnames () { 152 | return this.domains || [] 153 | } 154 | 155 | get additionalUrls () { 156 | var urls = [] 157 | this.hostnames.forEach(hostname => { 158 | if (this.config.httpMirror) { 159 | urls.push('http://' + hostname) 160 | } 161 | }) 162 | return urls 163 | } 164 | 165 | get storageDirectory () { 166 | return path.join(this.config.directory, this.hyperdriveKey) 167 | } 168 | } 169 | 170 | class HomebaseProxyConfig { 171 | constructor (canonical, config) { 172 | for (var k in canonical) { 173 | this[k] = canonical[k] 174 | } 175 | } 176 | 177 | get id () { 178 | return 'proxy-' + this.from 179 | } 180 | 181 | get vhostType () { 182 | return 'proxy' 183 | } 184 | 185 | get hostnames () { 186 | return [this.from] 187 | } 188 | } 189 | 190 | class HomebaseRedirectConfig { 191 | constructor (canonical, config) { 192 | for (var k in canonical) { 193 | this[k] = canonical[k] 194 | } 195 | } 196 | 197 | get id () { 198 | return 'redirect-' + this.from 199 | } 200 | 201 | get vhostType () { 202 | return 'redirect' 203 | } 204 | 205 | get hostnames () { 206 | return [this.from] 207 | } 208 | } 209 | 210 | function getHyperdriveKey (url) { 211 | return /^(hyper:\/\/)?([0-9a-f]{64})\/?$/i.exec(url)[2] 212 | } 213 | 214 | function validateHyperdriveCfg (hyperdrive, config) { 215 | // regular attributes 216 | check(hyperdrive && typeof hyperdrive === 'object', 'hyperdrives.* must be an object, see https://github.com/beakerbrowser/homebase/tree/master#hyperdrives', hyperdrive) 217 | hyperdrive.domains = (!hyperdrive.domains || Array.isArray(hyperdrive.domains)) ? hyperdrive.domains : [hyperdrive.domains] 218 | check(isHyperdriveUrl(hyperdrive.url), 'hyperdrives.*.url must be a valid hyperdrive url, see https://github.com/beakerbrowser/homebase/tree/master#hyperdrivesurl', hyperdrive.url, 'invalidUrl') 219 | 220 | // aliases 221 | if (hyperdrive.domain && !hyperdrive.domains) { 222 | hyperdrive.domains = hyperdrive.domain 223 | delete hyperdrive.domain 224 | hyperdrive.domains = Array.isArray(hyperdrive.domains) ? hyperdrive.domains : [hyperdrive.domains] 225 | } 226 | 227 | // regular attributes 228 | if (hyperdrive.domains) { 229 | hyperdrive.domains.forEach(domain => { 230 | check(isDomain(domain), 'hyperdrives.*.domains.* must be domain names, see https://github.com/beakerbrowser/homebase/tree/master#hyperdrivesdomains', domain, 'invalidDomain') 231 | }) 232 | } 233 | } 234 | 235 | module.exports = { 236 | HomebaseConfig, 237 | HomebaseProxyConfig, 238 | HomebaseRedirectConfig 239 | } 240 | 241 | // internal methods 242 | // = 243 | 244 | function validate (config) { 245 | // deprecated attributes 246 | if ('domain' in config) { 247 | console.log('FYI, the domain attribute in your homebase.yml was deprecated in v2.0.0. See https://github.com/beakerbrowser/homebase/tree/master#v200') 248 | check(typeof config.domain === 'string', 'domain must be a string, see https://github.com/beakerbrowser/homebase/tree/master#domain') 249 | } 250 | 251 | if ('directory' in config) check(typeof config.directory === 'string', 'directory must be a string, see https://github.com/beakerbrowser/homebase/tree/master#directory') 252 | if ('httpMirror' in config) check(typeof config.httpMirror === 'boolean', 'httpMirror must be true or false, see https://github.com/beakerbrowser/homebase/tree/master#httpmirror') 253 | if ('ports' in config) check(config.ports && typeof config.ports === 'object', 'ports must be an object containing .http and/or .https, see https://github.com/beakerbrowser/homebase/tree/master#ports') 254 | if ('ports' in config && 'http' in config.ports) check(typeof config.ports.http === 'number', 'ports.http must be a number, see https://github.com/beakerbrowser/homebase/tree/master#portshttp') 255 | if ('ports' in config && 'https' in config.ports) check(typeof config.ports.https === 'number', 'ports.https must be a number, see https://github.com/beakerbrowser/homebase/tree/master#portshttp') 256 | if ('dashboard' in config) check(typeof config.dashboard === 'object' || config.dashboard === false, 'dashboard must be an object or false, see https://github.com/beakerbrowser/homebase/tree/master#dashboard') 257 | if (config.dashboard && 'port' in config.dashboard) check(typeof config.dashboard.port === 'number', 'dashboard.port must be a number, see https://github.com/beakerbrowser/homebase/tree/master#dashboardport') 258 | if (config.hyperdrives) { 259 | config.hyperdrives = Array.isArray(config.hyperdrives) ? config.hyperdrives : [config.hyperdrives] 260 | config.hyperdrives.forEach(hyperdriveCfg => validateHyperdriveCfg(hyperdriveCfg, config)) 261 | } 262 | if (config.proxies) { 263 | config.proxies = Array.isArray(config.proxies) ? config.proxies : [config.proxies] 264 | config.proxies.forEach(proxy => { 265 | check(isDomain(proxy.from), 'proxies.*.from must be a domain name, see https://github.com/beakerbrowser/homebase/tree/master#proxiesfrom', proxy.from) 266 | check(isOrigin(proxy.to), 'proxies.*.to must be a target origin, see https://github.com/beakerbrowser/homebase/tree/master#proxiesto', proxy.to) 267 | }) 268 | } 269 | if (config.redirects) { 270 | config.redirects = Array.isArray(config.redirects) ? config.redirects : [config.redirects] 271 | config.redirects.forEach(redirect => { 272 | check(isDomain(redirect.from), 'redirects.*.from must be a domain name, see https://github.com/beakerbrowser/homebase/tree/master#redirectsfrom', redirect.from) 273 | check(isOrigin(redirect.to), 'redirects.*.to must be a target origin, see https://github.com/beakerbrowser/homebase/tree/master#redirectsto', redirect.to) 274 | 275 | // remove trailing slash 276 | redirect.to = redirect.to.replace(/\/$/, '') 277 | }) 278 | } 279 | } 280 | 281 | function check (assertion, error, value, errorKey) { 282 | if (!assertion) { 283 | var err = new ConfigError(error) 284 | err.value = value 285 | if (errorKey) { 286 | err[errorKey] = true 287 | } 288 | throw err 289 | } 290 | } 291 | 292 | function isHyperdriveUrl (str) { 293 | if (typeof str !== 'string') return false 294 | return /^(hyper:\/\/)?([0-9a-f]{64})\/?$/i.test(str) 295 | } 296 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | class ExtendableError extends Error { 2 | constructor (msg) { 3 | super(msg) 4 | this.name = this.constructor.name 5 | this.message = msg 6 | if (typeof Error.captureStackTrace === 'function') { 7 | Error.captureStackTrace(this, this.constructor) 8 | } else { 9 | this.stack = (new Error(msg)).stack 10 | } 11 | } 12 | } 13 | 14 | exports.ConfigError = class ConfigError extends ExtendableError { 15 | constructor (msg) { 16 | super(msg || 'Configuration error') 17 | this.configError = true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/hyperdrive-directory-listing-page.js: -------------------------------------------------------------------------------- 1 | const joinPaths = require('path').join 2 | 3 | module.exports = renderDirectoryListingPage 4 | 5 | const styles = `` 22 | 23 | async function renderDirectoryListingPage (drive, dirPath) { 24 | // list files 25 | var names = [] 26 | try { names = await drive.promises.readdir(dirPath) } catch (e) {} 27 | 28 | // stat each file 29 | var entries = await Promise.all(names.map(async (name) => { 30 | var entry 31 | var entryPath = joinPaths(dirPath, name) 32 | try { entry = await drive.promises.stat(entryPath) } catch (e) { return false } 33 | entry.path = entryPath 34 | entry.name = name 35 | return entry 36 | })) 37 | entries = entries.filter(Boolean) 38 | 39 | // sort the listing 40 | entries.sort((a, b) => { 41 | // directories on top 42 | if (a.isDirectory() && !b.isDirectory()) return -1 43 | if (!a.isDirectory() && b.isDirectory()) return 1 44 | // alphabetical after that 45 | return a.name.localeCompare(b.name) 46 | }) 47 | 48 | // show the updog if path is not top 49 | var updog = '' 50 | if (['/', '', '..'].includes(dirPath) === false) { 51 | updog = `
..
` 52 | } 53 | 54 | // render entries 55 | var totalFiles = 0 56 | entries = entries.map(entry => { 57 | totalFiles++ 58 | var url = makeSafe(entry.path) 59 | if (!url.startsWith('/')) url = '/' + url // all urls should have a leading slash 60 | if (entry.isDirectory() && !url.endsWith('/')) url += '/' // all dirs should have a trailing slash 61 | var type = entry.isDirectory() ? 'directory' : 'file' 62 | return `
${makeSafe(entry.name)}
` 63 | }).join('') 64 | 65 | // render summary 66 | var summary = `
${totalFiles} ${pluralize(totalFiles, 'file')}
` 67 | 68 | // render final 69 | return '' + styles + updog + entries + summary 70 | } 71 | 72 | function pluralize (num, base, suffix = 's') { 73 | if (num === 1) { 74 | return base 75 | } 76 | return base + suffix 77 | } 78 | 79 | function makeSafe (str) { 80 | return str.replace(//g, '>').replace(/&/g, '&').replace(/"/g, '') 81 | } 82 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const {HomebaseConfig} = require('./config') 2 | const {start, configure} = require('./server') 3 | 4 | module.exports = { 5 | HomebaseConfig, 6 | start, 7 | configure 8 | } 9 | -------------------------------------------------------------------------------- /lib/metrics.js: -------------------------------------------------------------------------------- 1 | var prom = require('prom-client') 2 | var responseTime = require('response-time') 3 | 4 | var metric = { 5 | https_hits: new prom.Counter({name: 'app_https_hits', help: 'Number of https requests received', labelNames: ['hostname', 'path']}), 6 | respTime: new prom.Summary({name: 'app_https_response_time_ms', help: 'Response time in ms', labelNames: ['hostname', 'path']}), 7 | datUploadSpeed: new prom.Gauge({name: 'app_dat_upload_speed', help: 'Bytes uploaded per second', labelNames: ['dat']}), 8 | datDownloadSpeed: new prom.Gauge({name: 'app_dat_download_speed', help: 'Bytes downloaded per second', labelNames: ['dat']}), 9 | datPeers: new prom.Gauge({name: 'app_dat_peers', help: 'Number of peers on the network', labelNames: ['dat']}) 10 | } 11 | 12 | module.exports = {hits: hits, respTime: respTime, getMetrics: getMetrics} 13 | 14 | function hits (vhostCfg) { 15 | return function (req, res, next) { 16 | metric.https_hits.inc({hostname: vhostCfg.id, path: req.path}) 17 | 18 | next() 19 | } 20 | } 21 | 22 | function respTime (vhostCfg) { 23 | return responseTime(function (req, res, time) { 24 | metric.respTime.labels(vhostCfg.id, req.path).observe(time) 25 | }) 26 | } 27 | 28 | function getMetrics () { 29 | return prom.register.metrics() 30 | } 31 | -------------------------------------------------------------------------------- /lib/mime.js: -------------------------------------------------------------------------------- 1 | const identifyFiletype = require('identify-filetype') 2 | const mime = require('mime') 3 | const through2 = require('through2') 4 | 5 | mime.default_type = 'text/plain' 6 | 7 | var identify = 8 | exports.identify = function (name, chunk) { 9 | // try to identify the type by the chunk contents 10 | var mimeType 11 | var identifiedExt = (chunk) ? identifyFiletype(chunk) : false 12 | if (identifiedExt) { 13 | mimeType = mime.getType(identifiedExt) 14 | } 15 | if (!mimeType) { 16 | // fallback to using the entry name 17 | mimeType = mime.getType(name) 18 | } 19 | 20 | // hackish fix 21 | // the svg test can be a bit aggressive: html pages with 22 | // inline svgs can be falsely interpretted as svgs 23 | // double check that 24 | if (identifiedExt === 'svg' && mime.getType(name) === 'text/html') { 25 | return 'text/html' 26 | } 27 | 28 | return mimeType 29 | } 30 | 31 | exports.identifyStream = function (name, cb) { 32 | var first = true 33 | return through2(function (chunk, enc, cb2) { 34 | if (first) { 35 | first = false 36 | cb(identify(name, chunk)) 37 | } 38 | this.push(chunk) 39 | cb2() 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const fs = require('fs') 3 | const express = require('express') 4 | const vhost = require('vhost') 5 | const compression = require('compression') 6 | const mkdirp = require('mkdirp') 7 | const figures = require('figures') 8 | const chalk = require('chalk') 9 | const metrics = require('./metrics') 10 | const vhostMethods = { 11 | hyperdrive: require('./vhosts/hyperdrive'), 12 | proxy: require('./vhosts/proxy'), 13 | redirect: require('./vhosts/redirect') 14 | } 15 | const packageJson = require('../package.json') 16 | 17 | // constants 18 | // = 19 | 20 | const IS_DEBUG = (['debug', 'staging', 'test'].indexOf(process.env.NODE_ENV) !== -1) 21 | const ENABLED = chalk.green(figures.tick + ' enabled') 22 | const DISABLED = chalk.red(figures.cross + ' disabled') 23 | const BULLET = chalk.dim(figures.play) 24 | 25 | // globals 26 | // = 27 | 28 | var app 29 | var activeRouter 30 | var metricsServer 31 | 32 | // exported api 33 | // = 34 | 35 | exports.start = async function (config, cb) { 36 | var server 37 | var plainServer 38 | 39 | mkdirp.sync(config.directory) 40 | 41 | console.log(` 42 | ${chalk.bold(`== Homebase ${packageJson.version} ==`)} 43 | 44 | ${BULLET} Directory: ${config.directory} 45 | ${BULLET} Ports: ${config.ports.http} ${chalk.dim('(HTTP)')} 46 | ${BULLET} HTTP mirror: ${config.httpMirror ? ENABLED : DISABLED} 47 | ${BULLET} Dashboard: ${config.dashboard ? ENABLED : DISABLED} 48 | `) 49 | 50 | // create server app 51 | app = express() 52 | exports.configure(config) 53 | app.use(compression()) 54 | app.use(function (req, res, next) { 55 | activeRouter(req, res, next) 56 | }) 57 | app.use((err, req, res, next) => { 58 | console.log(err) 59 | res.status(500).end() 60 | }) 61 | 62 | // start server 63 | server = http.createServer(app) 64 | server.listen(config.ports.http) 65 | if (plainServer) plainServer.on('error', onServerError) 66 | server.on('error', onServerError) 67 | function onServerError (err) { 68 | if (err.code === 'EACCES') { 69 | console.error(chalk.red(`ERROR: Failed to bind to ${chalk.bold(`port ${err.port}`)} (EACCES) 70 | 71 | Make sure the ${chalk.bold(`port is not in use`)} and that you ${chalk.bold(`have permission`)} to use it. 72 | See ${chalk.underline(`https://github.com/beakerbrowser/homebase/tree/master#port-setup-eacces-error`)}`)) 73 | process.exit(1) 74 | } 75 | throw err 76 | } 77 | if (cb) { 78 | server.once('listening', cb) 79 | } 80 | 81 | // watch the config file for changes 82 | var watcher 83 | if (config.configPath) { 84 | var prevTime = 0 85 | watcher = fs.watch(config.configPath, function () { 86 | fs.lstat(config.configPath, function (_, st) { 87 | var now = Date.now() 88 | if (now - prevTime > 100) { 89 | console.log(`\nDetected change to ${config.configPath}, reloading...\n`) 90 | config.readFromFile() 91 | watcher.close() 92 | server.close(() => { 93 | exports.start(config) 94 | }) 95 | } 96 | prevTime = now 97 | }) 98 | }) 99 | } 100 | 101 | return { 102 | config, 103 | close: cb => { 104 | if (watcher) watcher.close() 105 | server.close(cb) 106 | } 107 | } 108 | } 109 | 110 | exports.configure = function (config) { 111 | // create a new router 112 | activeRouter = express.Router() 113 | 114 | // add vhosts 115 | config.allVhosts.forEach(async vhostCfg => { 116 | var vhostServer = await vhostMethods[vhostCfg.vhostType].start(vhostCfg, config) 117 | vhostCfg.hostnames.forEach(hostname => activeRouter.use(vhost(hostname, vhostServer))) 118 | }) 119 | 120 | // metrics server 121 | if (metricsServer) { 122 | metricsServer.close() 123 | metricsServer = null 124 | } 125 | if (config.dashboard) { 126 | metricsServer = http.createServer((req, res) => res.end(metrics.getMetrics())) 127 | metricsServer.listen(config.dashboard.port) 128 | } 129 | } 130 | 131 | -------------------------------------------------------------------------------- /lib/vhosts/hyperdrive.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const HyperspaceClient = require('hyperspace/client') 3 | const HyperspaceServer = require('hyperspace/server') 4 | const hyperdrive = require('hyperdrive') 5 | const chalk = require('chalk') 6 | const parseRange = require('range-parser') 7 | const mime = require('../mime') 8 | const metrics = require('../metrics') 9 | const directoryListingPage = require('../hyperdrive-directory-listing-page') 10 | 11 | var hserver 12 | var hclient 13 | var hcorestore 14 | var activeHyperdrives = {} 15 | 16 | module.exports.start = async function (vhostCfg, config) { 17 | var server = express() 18 | 19 | await startHyperspace(config) 20 | 21 | // start the dat 22 | if (!activeHyperdrives[vhostCfg.id]) { 23 | activeHyperdrives[vhostCfg.id] = await loadDrive(vhostCfg.hyperdriveKey) 24 | } 25 | 26 | // setup the server routes 27 | server.use(metrics.hits(vhostCfg)) 28 | server.use(metrics.respTime(vhostCfg)) 29 | if (config.httpMirror) { 30 | server.use(createHttpMirror(vhostCfg)) 31 | } 32 | 33 | // log 34 | console.log(`${chalk.bold(`Serving`)}\n ${vhostCfg.url}`) 35 | if (vhostCfg.hostnames.length) { 36 | console.log(` ${chalk.dim(`at`)} ${vhostCfg.hostnames.join(', ')}`) 37 | } 38 | 39 | return server 40 | } 41 | 42 | module.exports.stop = async function (vhostCfg) { 43 | if (activeHyperdrives[vhostCfg.id]) { 44 | await activeHyperdrives[vhostCfg.id].promises.close() 45 | activeHyperdrives[vhostCfg.id] = null 46 | } 47 | 48 | // log 49 | console.log(`${chalk.bold(`Stopped serving`)} ${vhostCfg.url}`) 50 | } 51 | 52 | // internal methods 53 | // = 54 | 55 | var startHyperspacePromise 56 | function startHyperspace (config) { 57 | if (!startHyperspacePromise) { 58 | startHyperspacePromise = _startHyperspace(config) 59 | } 60 | return startHyperspacePromise 61 | } 62 | 63 | async function _startHyperspace (config) { 64 | const cleanup = async () => { 65 | console.log('Shutting down hyperspace, please wait...') 66 | if (hclient) await hclient.close() 67 | if (hserver) await hserver.close() 68 | } 69 | process.once('SIGINT', cleanup) 70 | process.once('SIGTERM', cleanup) 71 | 72 | hserver = new HyperspaceServer({ 73 | host: 'homebase', 74 | storage: config.hyperspaceDirectory 75 | }) 76 | await hserver.ready() 77 | 78 | hclient = new HyperspaceClient({ host: 'homebase' }) 79 | await hclient.ready() 80 | hcorestore = hclient.corestore() 81 | } 82 | 83 | async function loadDrive (key) { 84 | const drive = hyperdrive(hcorestore, Buffer.from(key, 'hex'), {sparse: false, extension: false}) 85 | await drive.promises.ready() 86 | await hclient.network.configure(drive.discoveryKey, { announce: true, lookup: true, flush: true }) 87 | return drive 88 | } 89 | 90 | function createHttpMirror (vhostCfg) { 91 | return async function (req, res) { 92 | var respondError = (code, status) => { 93 | res.status(code) 94 | res.end(code + ' ' + status) 95 | } 96 | const respondRedirect = (url) => { 97 | res.redirect(url) 98 | } 99 | var cspHeader = '' 100 | 101 | // validate request 102 | if (req.method !== 'GET' && req.method !== 'HEAD') { 103 | return respondError(405, 'Method Not Supported') 104 | } 105 | 106 | var drive = activeHyperdrives[vhostCfg.id] 107 | if (!drive) { 108 | return respondError(500, 'Hyperdrive not loaded') 109 | } 110 | 111 | // parse path 112 | var filepath = req.path 113 | if (!filepath) filepath = '/' 114 | if (filepath.indexOf('?') !== -1) filepath = filepath.slice(0, filepath.indexOf('?')) // strip off any query params 115 | var hasTrailingSlash = filepath.endsWith('/') 116 | 117 | // read the manifest (it's needed in a couple places) 118 | var manifest 119 | try { manifest = JSON.parse(await drive.promises.readFile('index.json')) } catch (e) { manifest = null } 120 | 121 | // read manifest CSP 122 | if (manifest && manifest.csp && typeof manifest.csp === 'string') { 123 | cspHeader = manifest.csp 124 | } 125 | 126 | // lookup entry 127 | var headers = {} 128 | var entry = await resolvePath(drive, filepath, hasTrailingSlash, req.headers.accept || req.headers.Accept) 129 | 130 | // handle folder 131 | if (entry && entry.isDirectory()) { 132 | // make sure there's a trailing slash 133 | if (!hasTrailingSlash) { 134 | return respondRedirect(`${filepath || ''}/`) 135 | } 136 | 137 | // directory listing 138 | res.set({ 139 | 'Content-Type': 'text/html', 140 | 'Content-Security-Policy': cspHeader, 141 | 'Allow-CSP-From': '*', 142 | 'Access-Control-Allow-Origin': '*', 143 | 'Cache-Control': 'no-cache' 144 | }) 145 | if (req.method === 'HEAD') { 146 | return res.status(204).end() 147 | } else { 148 | return res.status(200).end(await directoryListingPage(drive, filepath)) 149 | } 150 | } 151 | 152 | if (!entry) { 153 | return respondError(404, 'File Not Found') 154 | } 155 | 156 | // handle .goto redirects 157 | if (entry.path.endsWith('.goto') && entry.metadata.href) { 158 | try { 159 | let u = new URL(entry.metadata.href) // make sure it's a valid url 160 | return respondRedirect(entry.metadata.href) 161 | } catch (e) { 162 | // pass through 163 | } 164 | } 165 | 166 | // handle range 167 | headers['Accept-Ranges'] = 'bytes' 168 | var length 169 | var range = req.headers.Range || req.headers.range 170 | if (range) range = parseRange(entry.size, range) 171 | if (range && range.type === 'bytes') { 172 | range = range[0] // only handle first range given 173 | statusCode = 206 174 | length = (range.end - range.start + 1) 175 | headers['Content-Length'] = '' + length 176 | headers['Content-Range'] = 'bytes ' + range.start + '-' + range.end + '/' + entry.size 177 | } else { 178 | if (entry.size) { 179 | length = entry.size 180 | headers['Content-Length'] = '' + length 181 | } 182 | } 183 | 184 | Object.assign(headers, { 185 | 'Content-Security-Policy': cspHeader, 186 | 'Access-Control-Allow-Origin': '*', 187 | 'Allow-CSP-From': '*', 188 | 'Cache-Control': 'no-cache' 189 | }) 190 | 191 | var mimeType = mime.identify(entry.path) 192 | headers['Content-Type'] = mimeType 193 | res.set(headers) 194 | if (req.method === 'HEAD') { 195 | res.status(204) 196 | res.end() 197 | } else { 198 | res.status(200) 199 | drive.createReadStream(entry.path, range).pipe(res) 200 | } 201 | } 202 | } 203 | 204 | function acceptHeaderExtensions (accept) { 205 | var exts = [] 206 | var parts = (accept || '').split(',') 207 | if (parts.includes('text/html') || (parts.length === 1 && parts[0] === '*/*')) exts = exts.concat(['.html', '.md']) 208 | if (parts.includes('text/css')) exts.push('.css') 209 | if (parts.includes('image/*') || parts.includes('image/apng')) exts = exts.concat(['.png', '.jpg', '.jpeg', '.gif']) 210 | return exts 211 | } 212 | 213 | async function resolvePath (drive, filepath, hasTrailingSlash, acceptHeader) { 214 | // lookup entry 215 | var entry 216 | const tryStat = async (path) => { 217 | // abort if we've already found it 218 | if (entry) return 219 | // attempt lookup 220 | try { 221 | entry = await drive.promises.stat(path) 222 | entry.path = path 223 | } catch (e) {} 224 | } 225 | 226 | // do lookup 227 | if (hasTrailingSlash) { 228 | await tryStat(filepath + 'index.html') 229 | await tryStat(filepath + 'index.md') 230 | await tryStat(filepath) 231 | } else { 232 | await tryStat(filepath) 233 | for (let ext of acceptHeaderExtensions(acceptHeader)) { 234 | // fallback to different requested headers 235 | await tryStat(filepath + ext) 236 | } 237 | if (entry && entry.isDirectory()) { 238 | // unexpected directory, give the .html fallback a chance 239 | let dirEntry = entry 240 | entry = null 241 | await tryStat(filepath + '.html') // fallback to .html 242 | if (dirEntry && !entry) { 243 | // no .html fallback found, stick with directory that we found 244 | entry = dirEntry 245 | } 246 | } 247 | } 248 | 249 | return entry 250 | } -------------------------------------------------------------------------------- /lib/vhosts/proxy.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const chalk = require('chalk') 3 | const proxy = require('http-proxy').createProxyServer() 4 | 5 | module.exports.start = function (vhostCfg, config) { 6 | var server = express() 7 | 8 | server.all('*', function (req, res) { 9 | proxy.web(req, res, {target: vhostCfg.to}) 10 | }) 11 | 12 | // log 13 | console.log(`${chalk.bold(`Proxying`)} ${chalk.dim(`from`)} ${vhostCfg.from} ${chalk.dim(`to`)} ${vhostCfg.to}`) 14 | 15 | return server 16 | } 17 | 18 | module.exports.stop = function (vhostCfg) { 19 | // log 20 | console.log(`${chalk.bold(`Stopped proxying`)} ${chalk.dim(`from`)} ${vhostCfg.from} ${chalk.dim(`to`)} ${vhostCfg.to}`) 21 | } 22 | -------------------------------------------------------------------------------- /lib/vhosts/redirect.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const chalk = require('chalk') 3 | 4 | module.exports.start = function (vhostCfg, config) { 5 | var server = express() 6 | 7 | server.all('*', function (req, res) { 8 | res.redirect(vhostCfg.to + req.url) 9 | }) 10 | 11 | // log 12 | console.log(`${chalk.bold(`Redirecting`)} ${chalk.dim(`from`)} ${vhostCfg.from} ${chalk.dim(`to`)} ${vhostCfg.to}`) 13 | 14 | return server 15 | } 16 | 17 | module.exports.stop = function (vhostCfg) { 18 | // log 19 | console.log(`${chalk.bold(`Stopped redirecting`)} ${chalk.dim(`from`)} ${vhostCfg.from} ${chalk.dim(`to`)} ${vhostCfg.to}`) 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beaker/homebase", 3 | "version": "3.0.2", 4 | "description": "An easy-to-administer hosting server for Hyperdrive.", 5 | "main": "./lib/index.js", 6 | "bin": { 7 | "homebase": "./index.js" 8 | }, 9 | "directories": { 10 | "lib": "lib", 11 | "test": "test" 12 | }, 13 | "dependencies": { 14 | "chalk": "^2.4.1", 15 | "compression": "^1.7.3", 16 | "express": "^4.16.3", 17 | "figures": "^2.0.0", 18 | "http-body-parser": "^1.1.9", 19 | "http-proxy": "^1.17.0", 20 | "hyperdrive": "^10.14.1", 21 | "hyperspace": "^3.6.2", 22 | "identify-filetype": "^1.0.0", 23 | "is-domain-name": "^1.0.1", 24 | "is-http-url": "^2.0.0", 25 | "js-yaml": "^3.12.0", 26 | "le-store-certbot": "^2.1.3", 27 | "lodash.flatten": "^4.4.0", 28 | "lodash.pick": "^4.4.0", 29 | "mime": "^2.3.1", 30 | "mkdirp": "^0.5.1", 31 | "ms": "^2.1.1", 32 | "prom-client": "^11.0.0", 33 | "range-parser": "^1.2.0", 34 | "redirect-https": "^1.1.6", 35 | "response-time": "^2.3.2", 36 | "scoped-fs": "^1.1.3", 37 | "through2": "^2.0.3", 38 | "untildify": "^3.0.3", 39 | "vhost": "^3.0.2", 40 | "yargs": "^11.1.0" 41 | }, 42 | "devDependencies": { 43 | "ava": "^0.25.0", 44 | "request-promise-native": "^1.0.5", 45 | "rimraf": "^2.6.2", 46 | "tempy": "^0.2.1" 47 | }, 48 | "scripts": { 49 | "start": "node ./index.js", 50 | "test": "ava ./test/*-test.js -s" 51 | }, 52 | "repository": { 53 | "type": "git", 54 | "url": "git+https://github.com/beakerbrowser/homebase.git" 55 | }, 56 | "keywords": [ 57 | "hyper", 58 | "hyperdrive", 59 | "hyperspace", 60 | "server" 61 | ], 62 | "author": "Paul Frazee ", 63 | "license": "MIT", 64 | "bugs": { 65 | "url": "https://github.com/beakerbrowser/homebase/issues" 66 | }, 67 | "homepage": "https://github.com/beakerbrowser/homebase#readme" 68 | } 69 | -------------------------------------------------------------------------------- /test/config-test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const path = require('path') 3 | const os = require('os') 4 | const {HomebaseConfig} = require('../lib/config') 5 | 6 | const scaffold = (name) => path.join(__dirname, 'scaffold', name) 7 | const DATADIR = path.join(os.homedir(), '.homebase') 8 | 9 | test('empty config', t => { 10 | var cfg = new HomebaseConfig(scaffold('empty.yml')) 11 | 12 | t.deepEqual(cfg.canonical, {}) 13 | t.deepEqual(cfg.configPath, scaffold('empty.yml')) 14 | t.deepEqual(cfg.directory, DATADIR) 15 | t.deepEqual(cfg.httpMirror, false) 16 | t.deepEqual(cfg.ports, {http: 80}) 17 | t.deepEqual(cfg.dashboard, false) 18 | t.deepEqual(cfg.hyperdrives, []) 19 | t.deepEqual(cfg.proxies, []) 20 | t.deepEqual(cfg.redirects, []) 21 | t.deepEqual(cfg.hostnames, []) 22 | }) 23 | 24 | test('full config test', t => { 25 | var cfg = new HomebaseConfig(scaffold('full.yml')) 26 | 27 | t.deepEqual(cfg.canonical, { 28 | directory: '~/.homebase', 29 | httpMirror: true, 30 | ports: { 31 | http: 80 32 | }, 33 | dashboard: {port: 8089}, 34 | hyperdrives: [ 35 | { 36 | url: 'hyper://1f968afe867f06b0d344c11efc23591c7f8c5fb3b4ac938d6000f330f6ee2a03/', 37 | domains: [ 38 | 'mysite.com', 39 | 'my-site.com' 40 | ] 41 | }, 42 | { 43 | url: '868d6000f330f6967f06b3ee2a03811efc23591afe0d344cc7f8c5fb3b4ac91f', 44 | domains: ['othersite.com'] 45 | } 46 | ], 47 | proxies: [ 48 | {from: 'myproxy.com', to: 'https://mysite.com/'}, 49 | {from: 'foo.proxy.edu', to: 'http://localhost:8080/'}, 50 | {from: 'best-proxy-ever', to: 'http://127.0.0.1:123/'} 51 | ], 52 | redirects: [ 53 | {from: 'myredirect.com', to: 'https://mysite.com'}, 54 | {from: 'foo.redirect.edu', to: 'http://localhost:8080'}, 55 | {from: 'best-redirect-ever', to: 'http://127.0.0.1:123'} 56 | ] 57 | }) 58 | 59 | t.deepEqual(cfg.configPath, scaffold('full.yml')) 60 | t.deepEqual(cfg.directory, DATADIR) 61 | t.deepEqual(cfg.httpMirror, true) 62 | t.deepEqual(cfg.ports, { 63 | http: 80 64 | }) 65 | t.deepEqual(cfg.dashboard, {port: 8089}) 66 | t.deepEqual(extractHyperdriveCfg(cfg.hyperdrives[0]), { 67 | id: 'hyperdrive-1f968afe867f06b0d344c11efc23591c7f8c5fb3b4ac938d6000f330f6ee2a03', 68 | vhostType: 'hyperdrive', 69 | url: 'hyper://1f968afe867f06b0d344c11efc23591c7f8c5fb3b4ac938d6000f330f6ee2a03/', 70 | domains: [ 71 | 'mysite.com', 72 | 'my-site.com' 73 | ], 74 | hostnames: ['mysite.com', 'my-site.com'], 75 | hyperdriveKey: '1f968afe867f06b0d344c11efc23591c7f8c5fb3b4ac938d6000f330f6ee2a03' 76 | }) 77 | t.deepEqual(extractHyperdriveCfg(cfg.hyperdrives[1]), { 78 | id: 'hyperdrive-868d6000f330f6967f06b3ee2a03811efc23591afe0d344cc7f8c5fb3b4ac91f', 79 | vhostType: 'hyperdrive', 80 | url: '868d6000f330f6967f06b3ee2a03811efc23591afe0d344cc7f8c5fb3b4ac91f', 81 | domains: ['othersite.com'], 82 | hostnames: ['othersite.com'], 83 | hyperdriveKey: '868d6000f330f6967f06b3ee2a03811efc23591afe0d344cc7f8c5fb3b4ac91f' 84 | }) 85 | t.deepEqual(cfg.proxies.map(extractProxyCfg), [ 86 | {id: 'proxy-myproxy.com', vhostType: 'proxy', hostnames: ['myproxy.com'], from: 'myproxy.com', to: 'https://mysite.com/'}, 87 | {id: 'proxy-foo.proxy.edu', vhostType: 'proxy', hostnames: ['foo.proxy.edu'], from: 'foo.proxy.edu', to: 'http://localhost:8080/'}, 88 | {id: 'proxy-best-proxy-ever', vhostType: 'proxy', hostnames: ['best-proxy-ever'], from: 'best-proxy-ever', to: 'http://127.0.0.1:123/'} 89 | ]) 90 | t.deepEqual(cfg.redirects.map(extractRedirectCfg), [ 91 | {id: 'redirect-myredirect.com', vhostType: 'redirect', hostnames: ['myredirect.com'], from: 'myredirect.com', to: 'https://mysite.com'}, 92 | {id: 'redirect-foo.redirect.edu', vhostType: 'redirect', hostnames: ['foo.redirect.edu'], from: 'foo.redirect.edu', to: 'http://localhost:8080'}, 93 | {id: 'redirect-best-redirect-ever', vhostType: 'redirect', hostnames: ['best-redirect-ever'], from: 'best-redirect-ever', to: 'http://127.0.0.1:123'} 94 | ]) 95 | t.deepEqual(cfg.hostnames.slice().sort(), ['mysite.com', 'my-site.com', 'othersite.com', 'myproxy.com', 'foo.proxy.edu', 'best-proxy-ever', 'myredirect.com', 'foo.redirect.edu', 'best-redirect-ever'].sort()) 96 | }) 97 | 98 | test('can do (mostly) everything disabled', t => { 99 | var cfg = new HomebaseConfig(scaffold('everything-disabled.yml')) 100 | 101 | t.deepEqual(cfg.canonical, { 102 | httpMirror: false, 103 | dashboard: false, 104 | }) 105 | t.deepEqual(cfg.configPath, scaffold('everything-disabled.yml')) 106 | t.deepEqual(cfg.directory, DATADIR) 107 | t.deepEqual(cfg.httpMirror, false) 108 | t.deepEqual(cfg.ports, {http: 80}) 109 | t.deepEqual(cfg.dashboard, false) 110 | t.deepEqual(cfg.hyperdrives, []) 111 | t.deepEqual(cfg.proxies, []) 112 | t.deepEqual(cfg.redirects, []) 113 | t.deepEqual(cfg.hostnames, []) 114 | }) 115 | 116 | function extractHyperdriveCfg (cfg) { 117 | return { 118 | id: cfg.id, 119 | vhostType: cfg.vhostType, 120 | hostnames: cfg.hostnames, 121 | hyperdriveKey: cfg.hyperdriveKey, 122 | url: cfg.url, 123 | domains: cfg.domains 124 | } 125 | } 126 | 127 | function extractProxyCfg (cfg) { 128 | return { 129 | id: cfg.id, 130 | vhostType: cfg.vhostType, 131 | hostnames: cfg.hostnames, 132 | from: cfg.from, 133 | to: cfg.to 134 | } 135 | } 136 | 137 | function extractRedirectCfg (cfg) { 138 | return { 139 | id: cfg.id, 140 | vhostType: cfg.vhostType, 141 | hostnames: cfg.hostnames, 142 | from: cfg.from, 143 | to: cfg.to 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /test/scaffold/empty.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beakerbrowser/homebase/706d94ffe80e9f88b52f2419be8cf1fb3eaf3546/test/scaffold/empty.yml -------------------------------------------------------------------------------- /test/scaffold/everything-disabled.yml: -------------------------------------------------------------------------------- 1 | httpMirror: false 2 | dashboard: false 3 | -------------------------------------------------------------------------------- /test/scaffold/full.yml: -------------------------------------------------------------------------------- 1 | directory: ~/.homebase # where your data will be stored 2 | httpMirror: true # enables http mirrors of the dats 3 | ports: 4 | http: 80 # HTTP port for redirects or non-SSL serving 5 | dashboard: # set to false to disable 6 | port: 8089 # port for accessing the metrics dashboard 7 | 8 | # enter your hosted hyperdrives here 9 | hyperdrives: 10 | - url: hyper://1f968afe867f06b0d344c11efc23591c7f8c5fb3b4ac938d6000f330f6ee2a03/ 11 | domains: 12 | - mysite.com 13 | - my-site.com 14 | - url: 868d6000f330f6967f06b3ee2a03811efc23591afe0d344cc7f8c5fb3b4ac91f 15 | domain: 16 | - othersite.com 17 | 18 | # enter any proxied routes here 19 | proxies: 20 | - from: myproxy.com 21 | to: https://mysite.com/ 22 | - from: foo.proxy.edu 23 | to: http://localhost:8080/ 24 | - from: best-proxy-ever 25 | to: http://127.0.0.1:123/ 26 | 27 | # enter any redirect routes here 28 | redirects: 29 | - from: myredirect.com 30 | to: https://mysite.com/ 31 | - from: foo.redirect.edu 32 | to: http://localhost:8080/ 33 | - from: best-redirect-ever 34 | to: http://127.0.0.1:123/ -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const tempy = require('tempy') 3 | const request = require('request-promise-native') 4 | const {start} = require('../lib/server.js') 5 | const {HomebaseConfig} = require('../lib/config') 6 | 7 | var portCounter = 10000 8 | 9 | exports.createServer = function (configData) { 10 | var configPath = tempy.file({extension: 'yml'}) 11 | fs.writeFileSync(configPath, configData) 12 | 13 | var config = new HomebaseConfig(configPath) 14 | config.canonical.ports = { 15 | http: ++portCounter, 16 | https: ++portCounter 17 | } 18 | 19 | var server = start(config) 20 | server.req = request.defaults({ 21 | baseUrl: `http://127.0.0.1:${config.ports.http}`, 22 | resolveWithFullResponse: true, 23 | simple: false 24 | }) 25 | 26 | return server 27 | } 28 | --------------------------------------------------------------------------------