├── .dockerignore
├── .gitignore
├── Dockerfile
├── Makefile
├── README.md
├── app.js
├── bin
└── www
├── kube
├── api-deployment.yaml
└── api-svc.yaml
├── package.json
├── public
└── stylesheets
│ └── style.css
├── routes
├── healthcheck.js
├── index.js
└── users.js
└── views
├── error.pug
├── index.pug
├── layout.pug
├── opensearch.pug
└── welcome.pug
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://docs.npmjs.com/cli/shrinkwrap#caveats
27 | node_modules
28 |
29 | # Debug log from npm
30 | npm-debug.log
31 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:alpine
2 |
3 | # Create app directory
4 | RUN mkdir -p /usr/src/app
5 | WORKDIR /usr/src/app
6 |
7 | # Install app dependencies
8 | COPY package.json /usr/src/app/
9 | RUN npm install
10 |
11 | # Bundle app source
12 | COPY . /usr/src/app
13 |
14 | EXPOSE 3000
15 | CMD [ "npm", "start" ]
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | docker build -t quay.io/openai/gonode .
3 |
4 | push: build
5 | docker push quay.io/openai/gonode
6 |
7 | run: build
8 | docker run -it -p 3000:5000 -e AWS_DEFAULT_REGION=us-west-2 quay.io/openai/gonode
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Status:** Archive (code is provided as-is, no updates expected)
2 |
3 | Use this service to quickly alias and redirect to long URLs
4 |
5 | ## Setup
6 | ### Server setup
7 | Recommended to use Heroku to host the service if you don't care about security. Should take about 5 minutes to stand up. Otherwise you can put it on a Kubernetes cluster.
8 | 1. Create a Firebase project
9 | 1. Export Firebase creds on your [Firebase service account](https://console.developers.google.com/iam-admin/serviceaccounts/) as a json file
10 | 1. Create a new Heroku app and connect it to this git repo or your fork of it
11 | 1. Make json file's creds available as environment variables in your deployment environment (eg. through Heroku dashboard)
12 | * `FIREBASE_PROJECT_ID`
13 | * `FIREBASE_CLIENT_EMAIL`
14 | * `FIREBASE_DATABASE_NAME`
15 | * `FIREBASE_PRIVATE_KEY` which can have literal `\n` instead of actual newlines
16 | 1. Deploy!
17 |
18 | ### Client setup
19 | Add this site to your Chrome search engines by scrolling down until you see three empty boxes. Enter values go, go, and go.corp.mycompany.com/%s, respectively.
20 |
21 | ### Advanced setup
22 | To get the most benefit from the service, you should setup a DNS entry
23 | on your local network, `go.corp.mycompany.com`. Make sure that
24 | `corp.mycompany.com` is in the search domains for each user on the
25 | network. This is usually easily accomplished by configuring your DHCP
26 | server. Now, simply typing `go` into your browser should take you to
27 | the service, where you can register shortcuts. Those
28 | shortcuts will also be available by typing `go/shortcut`.
29 |
30 | On a mac, you can add the domain to your DNS search paths
31 | ```
32 | networksetup -setsearchdomains Wi-Fi corp.example.com
33 | ```
34 | However, this will require all bare names to hang instead of failing instantly, so you may wish to stick with the Chrome-only solution.
35 |
36 | ## Use
37 | When you type in the Chrome URL bar go<TAB> anything you type afterwards will bring you to that alias here.
38 | You can then paste your link in the box and the next time you visit this alias, it will bring you straight to the site you pasted before.
39 |
40 | You can override any alias by visiting eg., #{path}/myalias/mynewsite.com
41 |
42 | ## Develop
43 | Export your firebase credentials as appropriate environment variables. You'll need:
44 | * `FIREBASE_PROJECT_ID`
45 | * `FIREBASE_CLIENT_EMAIL`
46 | * `FIREBASE_DATABASE_NAME`
47 | * `FIREBASE_PRIVATE_KEY` which can have literal `\n` instead of actual newlines
48 |
49 | ```
50 | npm install
51 | npm install -g devtool
52 | npm run dev
53 | ```
54 |
55 | Visit http://localhost:3000/myalias
56 |
57 | ## Test in docker
58 | ```
59 | make run
60 | ```
61 |
62 | ## Deploy
63 | ```
64 | kubectl apply -f kube/api-deployment.yaml
65 | kubectl apply -f kube/api-svc.yaml
66 | ```
67 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var path = require('path');
3 | var favicon = require('serve-favicon');
4 | var logger = require('morgan');
5 | var cookieParser = require('cookie-parser');
6 | var bodyParser = require('body-parser');
7 |
8 | var index = require('./routes/index');
9 | var users = require('./routes/users');
10 | var healthcheck = require('./routes/healthcheck');
11 |
12 | var app = express();
13 |
14 | // view engine setup
15 | app.set('views', path.join(__dirname, 'views'));
16 | app.set('view engine', 'pug');
17 |
18 | // uncomment after placing your favicon in /public
19 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
20 | app.use(logger('dev'));
21 | app.use(bodyParser.json());
22 | app.use(bodyParser.urlencoded({ extended: false }));
23 | app.use(cookieParser());
24 | app.use(express.static(path.join(__dirname, 'public')));
25 |
26 | app.use('/users', users);
27 | app.use('/healthcheck', healthcheck);
28 | app.use('/', index);
29 |
30 | // catch 404 and forward to error handler
31 | app.use(function(req, res, next) {
32 | var err = new Error('Not Found');
33 | err.status = 404;
34 | next(err);
35 | });
36 |
37 | // error handler
38 | app.use(function(err, req, res, next) {
39 | // set locals, only providing error in development
40 | res.locals.message = err.message;
41 | res.locals.error = req.app.get('env') === 'development' ? err : {};
42 |
43 | // render the error page
44 | res.status(err.status || 500);
45 | res.render('error');
46 | });
47 |
48 | module.exports = app;
49 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('go-alias:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '3000');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
--------------------------------------------------------------------------------
/kube/api-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: extensions/v1beta1
2 | kind: Deployment
3 | metadata:
4 | name: gonode
5 | namespace: system
6 | spec:
7 | replicas: 3
8 | template:
9 | metadata:
10 | labels:
11 | app: gonode
12 | spec:
13 | containers:
14 | - name: app
15 | image: quay.io/openai/gonode
16 | imagePullPolicy: Always
17 | ports:
18 | - containerPort: 3000
19 | env:
20 | - name: AWS_DEFAULT_REGION
21 | value: "us-west-2"
22 | - name: FIREBASE_PROJECT_ID
23 | valueFrom:
24 | secretKeyRef:
25 | name: firebase
26 | key: project-id
27 | - name: FIREBASE_DATABASE_NAME
28 | valueFrom:
29 | secretKeyRef:
30 | name: firebase
31 | key: database-name
32 | - name: FIREBASE_CLIENT_EMAIL
33 | valueFrom:
34 | secretKeyRef:
35 | name: firebase
36 | key: client-email
37 | - name: FIREBASE_PRIVATE_KEY
38 | valueFrom:
39 | secretKeyRef:
40 | name: firebase
41 | key: private-key
42 | readinessProbe:
43 | httpGet:
44 | path: /healthcheck
45 | port: 3000
46 | initialDelaySeconds: 3
47 | timeoutSeconds: 1
48 | restartPolicy: Always
49 | imagePullSecrets:
50 | - name: quay-login-secret
51 | nodeSelector:
52 | aws/region: us-west-2
53 |
--------------------------------------------------------------------------------
/kube/api-svc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: go
5 | namespace: system
6 | spec:
7 | ports:
8 | - port: 80 # the port that this service should serve on
9 | # the container on each pod to connect to, can be a name
10 | # (e.g. 'www') or a number (e.g. 80)
11 | targetPort: 3000
12 | nodePort: 30212
13 | protocol: TCP
14 | type: NodePort
15 | # just like the selector in the replication controller,
16 | # but this time it identifies the set of pods to load balance
17 | # traffic to.
18 | selector:
19 | app: gonode
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "go-alias",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "start": "node ./bin/www",
7 | "dev": "DEBUG=go-alias:* devtool bin/www -w"
8 | },
9 | "engines": {
10 | "node": "6.6.0",
11 | "npm": "3.10.3"
12 | },
13 | "dependencies": {
14 | "body-parser": "~1.17.1",
15 | "cookie-parser": "~1.4.3",
16 | "debug": "~2.6.3",
17 | "express": "~4.15.2",
18 | "firebase-admin": "^4.1.4",
19 | "firebase-encode": "0.0.5",
20 | "morgan": "~1.8.1",
21 | "pug": "~2.0.0-beta11",
22 | "serve-favicon": "~2.4.2"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/public/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 50px;
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4 | }
5 |
6 | a {
7 | color: #00B7FF;
8 | }
9 |
--------------------------------------------------------------------------------
/routes/healthcheck.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | /* GET users listing. */
5 | router.get('/', function(req, res, next) {
6 | res.send('ok');
7 | });
8 |
9 | module.exports = router;
10 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 |
2 | var encoder = require('firebase-encode');
3 | var express = require('express');
4 | var router = express.Router();
5 |
6 | var admin = require("firebase-admin");
7 |
8 | // Handle escaped new lines in private key
9 | var firebasePrivateKey = process.env.FIREBASE_PRIVATE_KEY
10 | if (firebasePrivateKey.search(/\\n/) != -1) {
11 | firebasePrivateKey = firebasePrivateKey.replace(/\\n/g, '\n')
12 | }
13 |
14 | admin.initializeApp({
15 | credential: admin.credential.cert({
16 | projectId: process.env.FIREBASE_PROJECT_ID,
17 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
18 | privateKey: firebasePrivateKey
19 | }),
20 | databaseURL: `https://${process.env.FIREBASE_DATABASE_NAME}.firebaseio.com`
21 | });
22 |
23 |
24 | var db = admin.database();
25 | var linksRef = db.ref('links');
26 |
27 | /* GET home page. */
28 | router.get('/', function(req, res, next) {
29 | var fullUrl = req.protocol + '://' + req.get('host')
30 | res.render('welcome', {path: fullUrl})
31 | })
32 |
33 | /* For some reason this never gets pull even with link rel=search tag
34 | https://developer.mozilla.org/en-US/Add-ons/Creating_OpenSearch_plugins_for_Firefox
35 | */
36 | router.get('/opensearch.xml', function(req, res, next) {
37 | var fullUrl = req.protocol + '://' + req.get('host')
38 | res.render('opensearch', {path: fullUrl})
39 | })
40 |
41 | router.get('/:id', function(req, res, next) {
42 | linksRef.child(req.params.id).once('value').then(function(dataSnapshot) {
43 | if (dataSnapshot.exists()) {
44 | res.redirect(encoder.decode(dataSnapshot.val()))
45 | } else {
46 | res.render('index', { title: req.params.id })
47 | }
48 | });
49 |
50 | });
51 |
52 | function getId(path) {
53 | return path.split('/', 2)[1]
54 | }
55 |
56 | function getURL(path) {
57 | for (var i = 1; i < path.length; i++) {
58 | if (path[i] == '/') {
59 | return path.substring(i+1)
60 | }
61 | }
62 | return ''
63 | }
64 |
65 | router.get(/.*/, function(req, res, next) {
66 | var id = getId(req.path)
67 | var url = getURL(req.path)
68 | // make sure this ID isn't already taken
69 | // linksRef.child(req.params.id).once('value').then(function(dataSnapshot) {
70 | // if (dataSnapshot.exists()) {
71 | // res.redirect(dataSnapshot.val())
72 | // } else {
73 | // // Success.
74 | // }
75 | // });
76 |
77 | // Clobber
78 | if (!url.match(/https?/)) {
79 | url = 'http://' + url
80 | }
81 | linksRef.child(id).set(encoder.encode(url));
82 | res.render('index', { title: id, url: url })
83 |
84 |
85 | });
86 |
87 | module.exports = router
88 |
--------------------------------------------------------------------------------
/routes/users.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | /* GET users listing. */
5 | router.get('/', function(req, res, next) {
6 | res.send('respond with a resource');
7 | });
8 |
9 | module.exports = router;
10 |
--------------------------------------------------------------------------------
/views/error.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
7 |
--------------------------------------------------------------------------------
/views/index.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | a(href='/' + title)
5 | h1= title
6 | if (url)
7 | p Will now redirect to #{url}
8 | else
9 | p No URL set. Enter one:
10 | input#url.form-control(type='text', placeholder='url' name='url')
11 | button.btn.btn-primary(
12 | type='button',
13 | onclick='go()') Submit
14 |
15 | script.
16 | title='#{title}'
17 | function go() {
18 | window.location.href = `${title}/${document.getElementById("url").value}`
19 | }
20 | input = document.getElementById('url')
21 | if (input) {
22 | input.addEventListener('keypress', function (e) {
23 | var key = e.which || e.keyCode;
24 | if (key === 13) { // enter
25 | go()
26 | }
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/views/layout.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= title
5 | link(rel='stylesheet', href='/stylesheets/style.css')
6 | block head
7 | body
8 | block content
9 |
--------------------------------------------------------------------------------
/views/opensearch.pug:
--------------------------------------------------------------------------------
1 |
3 | xx
4 | Quickly visit link aliases!
5 | UTF-8
6 | data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADWElEQVR4AbSXAwxdQRBFa9u2g9q2bdsOa9u2Gdc2g9q2bWN7XzKb3Eyeqp+cj5nZ2ftm+SOEfUWNHiMdGANOg4/AgNdgJ2gPokf4Hy8kjgyGg8/SqRc3QZH/0fkajw4/uNi+gglgEBgmlcnxNwIGqA72gSoghvgTgJby9MaH46Di73ae0o61MBxEdImLJkJ9h0gYJzkCO88FNlLDtR5x9cB1iTkJDoIeoCZoCmaCN0rEML+O80iZjSK9S9x+8p8G5T1yJlYP8wMUdAtsKZPIKM5RTAqwEPwU3xPQEUSimFguuaOAvZRzkw6oJsqM8FJUXwCLKe6d+L+A8SAe+bKA9RLT305U9lMf32xbxxEHPKbOj0jZ6sjv9ZTECBnJlgBMcqneLdBQieBhK2WNfcj4CCQUu6cAKmtX8Ers38F8UEMmpBEOgQLSZhbZ69ikR8nYnTrzq0BFcI5+vwOdKC4SaAceiv8nWKYqUMIG81pPFkYAcRc8oN+bQXaKjwNGg0/gGvhBgpLqpJ+lXaAAKXtfEE3oq4ZiGkhI7TKAVeK3AipbJ+/psUMKSOyx3g3xQjalKBQzQc23OI7xMBmraQFS4qp6EuqXEridvl8C2cUfERwhXx+9Co7YvdppJI05aSgB8r0qzY86as8xwmE7UR6RcYLawXpIOU3YISDbehcBsflIt8ZKdnsVVoEM1CihTKzvwZPQT4DY6eS0hqQk4IcsmU+yhOJQw+yy1AzNj7v0O0wFklHsR2ssQcb9YBkJegjaqQOnkz0XhHOgYkgB3andUT3jHWaJrQA4RPaTss3OV0PR1S61IAEylDzf+tjAUlwBNV4N5WDRd79JIAHFZfQTABKrJfgYxLGB8cA3mgNZlIgYoL+Ufb34ue148MWeC+RbLEf6RvBSXUqq6WW0iQL28g7Glw114HQET2h7XQhSUMw5j5tzS7dNpKC6lGyk9a5jy4PTauLmUTHpXTrfp+N04mGqwRu5WDaVi2YPcJDO++ugnkeutephcoW5DUeUq7MJ4LNcxaN55BjOax2k/N3/BBXB8RB/xVqCBDRRq7jcqn+tipKekQYQJ0OjpgZanf4ispv2H9qtY6Z2f9GCiK7YD2g0MNOqx8wODZldQPwBKa4vQLvwcsSaBQD38AIuvFP0CQAAAABJRU5ErkJggg==
7 |
8 |
9 | #{path}/
10 |
11 |
--------------------------------------------------------------------------------
/views/welcome.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1 Welcome to the go service!
5 | p Use this service to easily visit long, complicated URLs.
6 | h2 Setup
7 | p.
8 | Add this site to your Chrome search engines by scrolling down until you see three empty boxes. Enter values go, go, and #{path}/%s, respectively.
9 | h2 Use
10 | p.
11 | When you type in the Chrome URL bar
12 | go<TAB>
13 | anything you type afterwards will bring you to that alias here.
14 | You can then paste your link in the box and the next time you visit this alias, it will bring you straight to the site you pasted before.
15 | p You can override any alias by visiting eg., #{path}/myalias/mynewsite.com
16 | h2 Feedback
17 | p This project is open source! Check out the
18 | a(href="https://github.com/openai/go-alias") Github
19 |
--------------------------------------------------------------------------------