mirror of
https://github.com/status-im/universal-links-handler.git
synced 2025-02-24 08:38:18 +00:00
Merge pull request #1 from status-im/features/add-android-asset-link
Add asset link & setup
This commit is contained in:
commit
832a3cba59
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
README.md
|
||||
tests/
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
# OS X
|
||||
.DS_Store*
|
||||
Icon?
|
||||
._*
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Linux
|
||||
.directory
|
||||
*~
|
||||
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
*.log
|
||||
*.gz
|
||||
|
||||
|
||||
# Coveralls
|
||||
coverage
|
||||
|
||||
# Benchmarking
|
||||
benchmarks/graphs
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM node:9-alpine
|
||||
|
||||
RUN mkdir -p /srv
|
||||
|
||||
ADD package.json /srv
|
||||
ADD package-lock.json /srv
|
||||
|
||||
WORKDIR /srv
|
||||
|
||||
RUN npm install
|
||||
|
||||
ADD . /srv
|
||||
|
||||
CMD npm start
|
50
README.md
Normal file
50
README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Universal links handler
|
||||
|
||||
App to handle universal links
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
You need to have `docker` & `docker-compose` installed
|
||||
|
||||
### Development
|
||||
|
||||
Start `docker-compose` with:
|
||||
|
||||
```
|
||||
docker-compose -p whatever up
|
||||
```
|
||||
|
||||
It will listen on port `8080` and mount the correct volumes so any change
|
||||
is then reflected.
|
||||
|
||||
`node_modules` are mounted as a volume so in case you change `package-lock.json`
|
||||
will need to be re-installed in the docker container.
|
||||
|
||||
### Production locally
|
||||
|
||||
Start `docker-compose` with:
|
||||
|
||||
```
|
||||
docker-compose -p whatever -f docker-compose.yml up
|
||||
```
|
||||
|
||||
Don't forget to rebuild the image if you made any changes
|
||||
|
||||
```
|
||||
docker-compose -p whatever -f docker-compose.yml build
|
||||
```
|
||||
|
||||
It will build the image and start the container listening on port `8080`
|
||||
|
||||
## Running the tests
|
||||
|
||||
To run the tests, first start the container, either in `production` or `development` mode.
|
||||
|
||||
Then you can run `bash tests/run.sh -u localhost:8080` or to run against against the live server
|
||||
`bash tests/run.sh -u http://get.status.im`
|
||||
|
||||
Uses the awesome `https://github.com/robwhitby/shakedown`
|
||||
|
||||
## Deployment
|
39
app.js
Normal file
39
app.js
Normal file
@ -0,0 +1,39 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
|
||||
var app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function(req, res, next) {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use(function(err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
module.exports = app;
|
90
bin/www
Executable file
90
bin/www
Executable file
@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require('../app');
|
||||
var debug = require('debug')('universal-links-handler:server');
|
||||
var http = require('http');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
}
|
11
docker-compose.override.yml
Normal file
11
docker-compose.override.yml
Normal file
@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
image: node:9-alpine
|
||||
working_dir: /srv
|
||||
volumes:
|
||||
- ./:/srv
|
||||
- node_modules:/srv/node_modules
|
||||
command: npm run watch
|
||||
volumes:
|
||||
node_modules:
|
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
@ -0,0 +1,9 @@
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
environment:
|
||||
PORT: 80
|
||||
ports:
|
||||
- 8080:80
|
3016
package-lock.json
generated
Normal file
3016
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "universal-links-handler",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www",
|
||||
"watch": "nodemon ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.3",
|
||||
"debug": "~2.6.9",
|
||||
"ejs": "~2.5.7",
|
||||
"express": "~4.16.0",
|
||||
"http-errors": "~1.6.2",
|
||||
"morgan": "~1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^1.17.5"
|
||||
}
|
||||
}
|
31
public/javascripts/app.js
Normal file
31
public/javascripts/app.js
Normal file
@ -0,0 +1,31 @@
|
||||
(function() {
|
||||
function buildPlayStoreUrl() {
|
||||
var androidId = $('meta[property="al:android:package"]').attr("content");
|
||||
return "https://play.google.com/store/apps/details?id=" + androidId;
|
||||
}
|
||||
|
||||
function buildItunesUrl() {
|
||||
|
||||
var iosId = $('meta[property="al:ios:app_store_id"]').attr("content");
|
||||
return "https://itunes.apple.com/app/id" + iosId;
|
||||
}
|
||||
function isAndroid(userAgent) {
|
||||
return userAgent.toLowerCase().indexOf("android") > -1;
|
||||
}
|
||||
|
||||
function isIOS(userAgent) {
|
||||
return userAgent.toLowerCase().indexOf("iphone") > -1;
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
var appStoreLink = $("#app-store-link");
|
||||
if (isAndroid(navigator.userAgent)) {
|
||||
appStoreLink.attr('href', buildPlayStoreUrl());
|
||||
} else if (isIOS(navigator.userAgent)) {
|
||||
appStoreLink.attr('href', buildItunesUrl());
|
||||
} else {
|
||||
appStoreLink.attr('href', buildPlayStoreUrl());
|
||||
}
|
||||
})
|
||||
}());
|
8
public/stylesheets/style.css
Normal file
8
public/stylesheets/style.css
Normal file
@ -0,0 +1,8 @@
|
||||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B7FF;
|
||||
}
|
5
resources/assetlinks.json
Normal file
5
resources/assetlinks.json
Normal file
@ -0,0 +1,5 @@
|
||||
[{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target" : { "namespace": "android_app", "package_name": "im.status.ethereum",
|
||||
"sha256_cert_fingerprints": ["29:58:6A:6B:A0:CA:FA:A5:38:AA:CE:EA:DF:60:55:1C:EC:22:E9:65:68:40:35:D4:75:B7:9A:0A:8B:13:E2:9F"] }
|
||||
}]
|
31
routes/index.js
Normal file
31
routes/index.js
Normal file
@ -0,0 +1,31 @@
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var assetLinks = require('../resources/assetlinks.json');
|
||||
|
||||
|
||||
router.get('/.well-known/assetlinks.json', function(req, res) {
|
||||
res.json(assetLinks);
|
||||
});
|
||||
|
||||
router.get('/chat/:chatType/:chatId', function(req, res, next) {
|
||||
res.render('index', {
|
||||
title: 'Status.im join ' + req.params.chatId + ' chat',
|
||||
path: req.originalUrl
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/user/:userId', function(req, res, next) {
|
||||
res.render('index', {
|
||||
title: 'Status.im view ' + req.params.userId + ' profile',
|
||||
path: req.originalUrl
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/browse/:url', function(req, res, next) {
|
||||
res.render('index', {
|
||||
title: 'Status.im browse ' + req.params.url + ' dapp',
|
||||
path: req.originalUrl
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
16
tests/run.sh
Normal file
16
tests/run.sh
Normal file
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
source tests/shakedown.sh
|
||||
|
||||
shakedown GET /.well-known/assetlinks.json
|
||||
status 200
|
||||
content_type 'application/json'
|
||||
contains 'sha256_cert_fingerprints'
|
||||
|
||||
shakedown GET /chat/public/abc
|
||||
status 200
|
||||
|
||||
shakedown GET /user/blah
|
||||
status 200
|
||||
|
||||
shakedown GET /browse/www.test.com
|
||||
status 200
|
149
tests/shakedown.sh
Normal file
149
tests/shakedown.sh
Normal file
@ -0,0 +1,149 @@
|
||||
#!/bin/bash
|
||||
set -u
|
||||
|
||||
BASE_URL=${SHAKEDOWN_URL:-""}
|
||||
CREDENTIALS=${SHAKEDOWN_CREDENTIALS:-""}
|
||||
|
||||
_usage() {
|
||||
echo '
|
||||
usage: $0 [options...]
|
||||
Options:
|
||||
-u <base URL> Base URL to test.
|
||||
-c <user:password> Credentials for HTTP authentication.
|
||||
'
|
||||
exit 1
|
||||
}
|
||||
|
||||
while getopts 'u:c:' OPTION
|
||||
do
|
||||
case $OPTION in
|
||||
u) BASE_URL="$OPTARG";;
|
||||
c) CREDENTIALS="$OPTARG";;
|
||||
*) _usage;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
echo "Starting shakedown of ${BASE_URL:-"[base URL not set]"}"
|
||||
|
||||
STATE=""
|
||||
FAIL_COUNT=0
|
||||
PASS_COUNT=0
|
||||
WORKING_DIR=$(mktemp -d -t shakedown.XXXXXX)
|
||||
RESPONSE_BODY="${WORKING_DIR}/body"
|
||||
RESPONSE_HEADERS="${WORKING_DIR}/headers"
|
||||
|
||||
AUTH=""
|
||||
if [ -n "${CREDENTIALS}" ]; then
|
||||
AUTH="--anyauth --user ${CREDENTIALS}"
|
||||
fi
|
||||
|
||||
CURL="curl -sS ${AUTH} -D ${RESPONSE_HEADERS} --connect-timeout 5 --max-time 30"
|
||||
|
||||
CRED=$(tput setaf 1 2> /dev/null)
|
||||
CGREEN=$(tput setaf 2 2> /dev/null)
|
||||
CDEFAULT=$(tput sgr0 2> /dev/null)
|
||||
|
||||
_pass() {
|
||||
echo " ${CGREEN}✔ ${1}${CDEFAULT}"
|
||||
}
|
||||
|
||||
_fail() {
|
||||
STATE="fail"
|
||||
echo " ${CRED}✘ ${1}${CDEFAULT}"
|
||||
}
|
||||
|
||||
_start_test() {
|
||||
_finish_test
|
||||
STATE="pass"
|
||||
}
|
||||
|
||||
_finish_test() {
|
||||
if [ "$STATE" = "pass" ]; then
|
||||
((PASS_COUNT++))
|
||||
elif [ "$STATE" = "fail" ]; then
|
||||
((FAIL_COUNT++))
|
||||
fi
|
||||
}
|
||||
|
||||
_finish() {
|
||||
_finish_test
|
||||
rm -rf "${WORKING_DIR}"
|
||||
echo
|
||||
MSG="Shakedown complete. ${PASS_COUNT} passed, ${FAIL_COUNT} failed."
|
||||
[[ ${FAIL_COUNT} -eq 0 ]] && echo "${CGREEN}${MSG}${CDEFAULT}" || echo "${CRED}${MSG} You're busted.${CDEFAULT}"
|
||||
exit ${FAIL_COUNT}
|
||||
}
|
||||
|
||||
trap _finish EXIT
|
||||
|
||||
# start test
|
||||
# $1 METHOD
|
||||
# $2 URL
|
||||
# $3..$n Custom CURL options
|
||||
shakedown() {
|
||||
_start_test
|
||||
METHOD="$1"
|
||||
URL="$2"
|
||||
if ! [[ $URL == http* ]]; then
|
||||
URL="${BASE_URL}${URL}"
|
||||
fi
|
||||
echo
|
||||
echo "${METHOD} ${URL}"
|
||||
METHOD_OPT="-X ${METHOD}"
|
||||
if [ "${METHOD}" = "HEAD" ]; then
|
||||
METHOD_OPT="-I"
|
||||
fi
|
||||
${CURL} ${METHOD_OPT} "${@:3}" "${URL}" > ${RESPONSE_BODY}
|
||||
}
|
||||
|
||||
# assertions
|
||||
|
||||
header() {
|
||||
grep -Fq "${1}" "${RESPONSE_HEADERS}" && _pass "header ${1}" || _fail "header ${1}"
|
||||
}
|
||||
|
||||
no_header() {
|
||||
grep -Fq "${1}" "${RESPONSE_HEADERS}" && _fail "no_header ${1}" || _pass "no_header ${1}"
|
||||
}
|
||||
|
||||
status() {
|
||||
STATUS_CODE=$(grep -Eo "^HTTP.+ [1-5][0-9][0-9] " ${RESPONSE_HEADERS} | grep -Eo '[1-5][0-9][0-9]' | tail -n1)
|
||||
[[ "${STATUS_CODE}" = "${1}" ]] && _pass "status ${1}" || _fail "status ${1} (actual: ${STATUS_CODE})"
|
||||
}
|
||||
|
||||
contains() {
|
||||
MSG="contains \"${1}\""
|
||||
grep -Fq "${1}" "${RESPONSE_BODY}" && _pass "${MSG}" || _fail "${MSG}"
|
||||
}
|
||||
|
||||
matches() {
|
||||
MSG="matches \"${1}\""
|
||||
grep -Eq "${1}" "${RESPONSE_BODY}" && _pass "${MSG}" || _fail "${MSG}"
|
||||
}
|
||||
|
||||
content_type() {
|
||||
CT_HEADER="$(_get_header 'Content-Type')"
|
||||
echo "${CT_HEADER}" | grep -Fq "${1}" && _pass "Content-Type: ${1}" || _fail "Content-Type: ${1} (actual: ${CT_HEADER})"
|
||||
}
|
||||
|
||||
header_contains() {
|
||||
HEADER_NAME=${1}
|
||||
HEADER="$(_get_header $HEADER_NAME)"
|
||||
echo "${HEADER}" | grep -Fq "${2}" && _pass "${HEADER_NAME}: ${2}" || _fail "${HEADER_NAME}: ${2} (actual: ${HEADER})"
|
||||
}
|
||||
|
||||
_get_header() {
|
||||
grep -F "${1}" "${RESPONSE_HEADERS}" | tr -d '\r'
|
||||
}
|
||||
|
||||
# debug
|
||||
|
||||
print_headers() {
|
||||
cat "${RESPONSE_HEADERS}"
|
||||
}
|
||||
|
||||
print_body() {
|
||||
cat "${RESPONSE_BODY}"
|
||||
}
|
||||
|
3
views/error.ejs
Normal file
3
views/error.ejs
Normal file
@ -0,0 +1,3 @@
|
||||
<h1><%= message %></h1>
|
||||
<h2><%= error.status %></h2>
|
||||
<pre><%= error.stack %></pre>
|
22
views/index.ejs
Normal file
22
views/index.ejs
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= title %></title>
|
||||
|
||||
<meta property="al:ios:url" content="app://get.status.im<%= path %>" />
|
||||
<meta property="al:ios:app_store_id" content="1178893006" />
|
||||
<meta property="al:ios:app_name" content="Status — Ethereum. Anywhere" />
|
||||
|
||||
<meta property="al:android:url" content="app://get.status.im<%= path %>" />
|
||||
<meta property="al:android:package" content="im.status.ethereum" />
|
||||
<meta property="al:android:app_name" content="Status — Ethereum. Anywhere" />
|
||||
|
||||
|
||||
<link rel='stylesheet' href='/stylesheets/style.css' />
|
||||
</head>
|
||||
<body>
|
||||
<a id='app-store-link' href='#'>Don't have status yet, try it</a>
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
|
||||
<script type='text/javascript' src='/javascripts/app.js'></script>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user