├── .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 | --------------------------------------------------------------------------------