@ -0,0 +1,22 @@ |
||||
{ |
||||
"attr-lowercase": true, |
||||
"attr-no-duplication": true, |
||||
"attr-unsafe-chars": true, |
||||
"attr-value-double-quotes": true, |
||||
"attr-value-not-empty": true, |
||||
"doctype-first": false, |
||||
"head-script-disabled": true, |
||||
"href-abs-or-rel": true, |
||||
"id-class-ad-disabled": true, |
||||
"id-class-value": true, |
||||
"id-unique": true, |
||||
"img-alt-require": true, |
||||
"space-tab-mixed-disabled": true, |
||||
"spec-char-escape": true, |
||||
"src-not-empty": true, |
||||
"style-disabled": true, |
||||
"tag-pair": true, |
||||
"tag-self-close": true, |
||||
"tagname-lowercase": true, |
||||
"title-require": true |
||||
} |
@ -0,0 +1,73 @@ |
||||
{ |
||||
"plugins": [], // npm modules to load |
||||
|
||||
"maxerr": false, |
||||
"raw-ignore-regex": false, |
||||
"attr-bans": [ |
||||
"align", |
||||
"background", |
||||
"bgcolor", |
||||
"border", |
||||
"frameborder", |
||||
"longdesc", |
||||
"marginwidth", |
||||
"marginheight", |
||||
"scrolling", |
||||
"style", |
||||
"width" |
||||
], |
||||
"indent-delta": false, |
||||
"indent-style": "nonmixed", |
||||
"indent-width": 2, |
||||
"indent-width-cont": false, |
||||
"spec-char-escape": true, |
||||
"text-ignore-regex": false, |
||||
"tag-bans": [ |
||||
"style", |
||||
"b" |
||||
], |
||||
"tag-close": true, |
||||
"tag-name-lowercase": true, |
||||
"tag-name-match": true, |
||||
"tag-self-close": false, |
||||
"doctype-first": false, |
||||
"doctype-html5": false, |
||||
"attr-name-style": "dash", |
||||
"attr-name-ignore-regex": false, |
||||
"attr-no-dup": true, |
||||
"attr-no-unsafe-char": true, |
||||
"attr-order": false, |
||||
"attr-quote-style": "double", |
||||
"attr-req-value": true, |
||||
"attr-new-line": false, |
||||
"attr-validate": true, |
||||
"id-no-dup": true, |
||||
"id-class-style": false, |
||||
"id-class-no-ad": true, |
||||
"class-style": false, |
||||
"class-no-dup": false, |
||||
"id-class-ignore-regex": false, |
||||
"img-req-alt": true, |
||||
"img-req-src": true, |
||||
"html-valid-content-model": true, |
||||
"head-valid-content-model": true, |
||||
"href-style": false, |
||||
"link-req-noopener": true, |
||||
"label-req-for": true, |
||||
"line-end-style": "lf", |
||||
"line-no-trailing-whitespace": true, |
||||
"line-max-len": false, |
||||
"line-max-len-ignore-regex": false, |
||||
"head-req-title": true, |
||||
"title-no-dup": true, |
||||
"title-max-len": 60, |
||||
"html-req-lang": false, |
||||
"lang-style": "case", |
||||
"fig-req-figcaption": false, |
||||
"focusable-tabindex-style": false, |
||||
"input-radio-req-name": true, |
||||
"input-req-label": false, |
||||
"table-req-caption": false, |
||||
"table-req-header": false, |
||||
"tag-req-attr": false |
||||
} |
@ -0,0 +1,13 @@ |
||||
repos: |
||||
- repo: https://github.com/jorisroovers/gitlint |
||||
rev: v0.15.1 |
||||
hooks: |
||||
- id: gitlint |
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs |
||||
rev: v1.1.1 |
||||
hooks: |
||||
- id: htmlhint |
||||
args: [--config, .htmlhintrc] |
||||
- id: htmllint |
||||
- id: dockerfile_lint |
@ -0,0 +1,15 @@ |
||||
FROM alpine as build |
||||
MAINTAINER lauri <lauri@pinecrypt.com> |
||||
RUN apk add --update npm nginx rsync bash |
||||
RUN npm install --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg xterm |
||||
COPY nginx.conf /etc/nginx/nginx.conf |
||||
EXPOSE 80 443 8443 |
||||
WORKDIR /var/lib/nginx/html/ |
||||
RUN rsync -avq /usr/lib/node_modules/font-awesome/fonts/ fonts/ |
||||
COPY static ./ |
||||
COPY templates templates |
||||
RUN nunjucks-precompile --include snippets --include views templates >> js/bundle.js |
||||
RUN bash -c 'cat /usr/lib/node_modules/{jquery/dist/jquery.min.js,tether/dist/js/tether.min.js,bootstrap/dist/js/bootstrap.min.js,node-forge/dist/forge.all.min.js,qrcode-svg/dist/qrcode.min.js,timeago/jquery.timeago.js,nunjucks/browser/nunjucks-slim.min.js,xterm/lib/xterm.js} >> js/bundle.js' |
||||
RUN bash -c 'cat /usr/lib/node_modules/{tether/dist/css/tether.min.css,bootstrap/dist/css/bootstrap.min.css,font-awesome/css/font-awesome.min.css,xterm/css/xterm.css} >> css/bundle.css' |
||||
COPY entrypoint.sh /entrypoint.sh |
||||
ENTRYPOINT /entrypoint.sh |
@ -0,0 +1,5 @@ |
||||
#!/bin/sh |
||||
while [ ! -f /var/lib/certidude/server-secrets/self_cert.pem ]; do |
||||
sleep 1 |
||||
done |
||||
exec nginx -g "daemon off; error_log /dev/stdout info;" |
@ -0,0 +1,156 @@ |
||||
user nginx; |
||||
worker_processes auto; |
||||
pid /run/nginx.pid; |
||||
include /etc/nginx/modules-enabled/*.conf; |
||||
|
||||
events { |
||||
worker_connections 768; |
||||
} |
||||
|
||||
http { |
||||
upstream read-write { |
||||
server 127.0.0.1:4001; |
||||
} |
||||
|
||||
upstream ocsp-responder { |
||||
server 127.0.0.1:5001; |
||||
} |
||||
|
||||
upstream builder { |
||||
server 127.0.0.1:7001; |
||||
} |
||||
|
||||
upstream event { |
||||
server 127.0.0.1:8001; |
||||
} |
||||
|
||||
resolver 127.0.0.11; |
||||
sendfile on; |
||||
tcp_nopush on; |
||||
tcp_nodelay on; |
||||
keepalive_timeout 65; |
||||
types_hash_max_size 2048; |
||||
|
||||
include /etc/nginx/mime.types; |
||||
default_type application/octet-stream; |
||||
|
||||
## |
||||
# SSL Settings |
||||
## |
||||
ssl_protocols TLSv1.2 TLSv1.3; |
||||
ssl_prefer_server_ciphers on; |
||||
|
||||
## |
||||
# Gzip Settings |
||||
## |
||||
gzip on; |
||||
|
||||
# Basic DoS prevention measures |
||||
limit_conn addr 100; |
||||
client_body_timeout 5s; |
||||
client_header_timeout 5s; |
||||
limit_conn_zone $binary_remote_addr zone=addr:10m; |
||||
|
||||
# Backend configuration |
||||
proxy_set_header Host $host; |
||||
proxy_set_header X-Real-IP $remote_addr; |
||||
proxy_set_header X-SSL-CERT $ssl_client_cert; |
||||
proxy_set_header X-Forwarded-Proto $scheme; |
||||
proxy_connect_timeout 600; |
||||
proxy_send_timeout 600; |
||||
proxy_read_timeout 600; |
||||
send_timeout 600; |
||||
|
||||
# To use CA-s own certificate for frontend and mutually authenticated connections |
||||
ssl_certificate /var/lib/certidude/server-secrets/self_cert.pem; |
||||
ssl_certificate_key /var/lib/certidude/server-secrets/self_key.pem; |
||||
|
||||
server { |
||||
# Section for serving insecure HTTP, note that this is suitable for |
||||
# OCSP, CRL-s etc which is already covered by PKI protection mechanisms. |
||||
|
||||
listen 80 default_server; |
||||
|
||||
# Proxy pass OCSP responder |
||||
location /api/ocsp/ { |
||||
proxy_pass http://ocsp-responder; |
||||
} |
||||
|
||||
# Event server |
||||
location /api/event/ { |
||||
proxy_buffering off; |
||||
proxy_cache off; |
||||
proxy_pass http://event; |
||||
} |
||||
|
||||
# Proxy pass to backend |
||||
location /api/ { |
||||
proxy_pass http://read-write; |
||||
} |
||||
} |
||||
|
||||
server { |
||||
# Section for accessing web interface over HTTPS |
||||
listen 127.0.0.1:1443 ssl http2 default_server; |
||||
|
||||
# HSTS header below should make sure web interface will be accessed over HTTPS only |
||||
# once it has been configured |
||||
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; |
||||
|
||||
#proxy pass event |
||||
location /api/event/ { |
||||
proxy_buffering off; |
||||
proxy_cache off; |
||||
proxy_pass http://event; |
||||
} |
||||
|
||||
#Proxy pass longpoll |
||||
location /api/longpoll/ { |
||||
proxy_buffering off; |
||||
proxy_cache off; |
||||
proxy_pass http://event; |
||||
} |
||||
|
||||
# OpenWrt image builder |
||||
location /api/build/ { |
||||
proxy_pass http://builder; |
||||
} |
||||
|
||||
# Proxy pass to backend |
||||
location /api/ { |
||||
proxy_pass http://read-write; |
||||
} |
||||
|
||||
# This is for Let's Encrypt enroll/renewal |
||||
location /.well-known/ { |
||||
alias /var/www/html/.well-known/; |
||||
} |
||||
} |
||||
|
||||
|
||||
server { |
||||
# Section for certificate authenticated HTTPS clients, |
||||
# for submitting information to CA eg. leases, |
||||
# for delivering scripts to clients, |
||||
# for exchanging messages over WebSockets |
||||
server_name $hostname; |
||||
listen 8443 ssl http2; |
||||
|
||||
# Enforce OCSP stapling for the server certificate |
||||
# Note that even nginx 1.14.0 doesn't immideately populate the OCSP cache |
||||
# You need to run separate cronjob to populate the OCSP response cache |
||||
ssl_stapling on; |
||||
ssl_stapling_verify on; |
||||
|
||||
# Allow client authentication with certificate, |
||||
# backend must still check if certificate was used for TLS handshake |
||||
ssl_verify_client optional; |
||||
ssl_client_certificate /var/lib/certidude/server-secrets/ca_cert.pem; |
||||
|
||||
# Proxy pass to backend |
||||
location /api/ { |
||||
proxy_pass http://read-write; |
||||
} |
||||
} |
||||
} |
||||
|
@ -0,0 +1,4 @@ |
||||
{ |
||||
"title": "502 Bad Gateway", |
||||
"description": "It seems the server had bit of a hiccup, perhaps this helps: systemctl restart certidude-backend && journalctl -f" |
||||
} |
@ -0,0 +1,104 @@ |
||||
|
||||
@keyframes fresh { |
||||
from { background-color: #ffc107; } |
||||
to { background-color: white; } |
||||
} |
||||
|
||||
.fresh { |
||||
animation-name: fresh; |
||||
animation-duration: 30s; |
||||
} |
||||
|
||||
.loader-container { |
||||
margin: 20% auto 0 auto; |
||||
text-align: center; |
||||
} |
||||
|
||||
.loader { |
||||
border: 16px solid #f3f3f3; /* Light grey */ |
||||
border-top: 16px solid #3498db; /* Blue */ |
||||
border-radius: 50%; |
||||
width: 120px; |
||||
height: 120px; |
||||
animation: spin 2s linear infinite; |
||||
display: inline-block; |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: 'PT Sans Narrow'; |
||||
font-style: normal; |
||||
font-weight: 400; |
||||
src: local('PT Sans Narrow'), local('PTSans-Narrow'), url('../fonts/pt-sans.woff2') format('woff2'); |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: 'Ubuntu Mono'; |
||||
font-style: normal; |
||||
font-weight: 400; |
||||
src: local('Ubuntu Mono'), local('UbuntuMono-Regular'), url('../fonts/ubuntu-mono.woff2') format('woff2'); |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: 'Gentium Basic'; |
||||
font-style: normal; |
||||
font-weight: 400; |
||||
src: local('Gentium Basic'), local('GentiumBasic'), url('../fonts/gentium-basic.woff2') format('woff2'); |
||||
} |
||||
|
||||
@keyframes spin { |
||||
0% { transform: rotate(0deg); } |
||||
100% { transform: rotate(360deg); } |
||||
} |
||||
|
||||
body, input { |
||||
} |
||||
|
||||
body, input { |
||||
} |
||||
|
||||
pre { |
||||
font-family: 'Ubuntu Mono'; |
||||
background: #333; |
||||
overflow: auto; |
||||
border: 1px solid #292929; |
||||
border-radius: 4px; |
||||
color: #ddd; |
||||
padding: 6px 10px; |
||||
} |
||||
|
||||
pre code a { |
||||
color: #eef; |
||||
} |
||||
|
||||
h1, h2 { |
||||
} |
||||
|
||||
|
||||
#view { |
||||
margin: 5em auto 5em auto; |
||||
|
||||
} |
||||
|
||||
footer div { |
||||
text-align: center; |
||||
} |
||||
|
||||
svg { |
||||
position: relative; |
||||
} |
||||
|
||||
.badge { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.disabled { |
||||
pointer-events: none; |
||||
opacity: 0.4; |
||||
cursor: not-allowed; |
||||
} |
||||
|
||||
#signed_certificates .filterable .fa-circle { color: #888888; } |
||||
#signed_certificates .filterable[data-state='online'] .fa-circle { color: #5cb85c; } |
||||
#signed_certificates .filterable[data-state='offline'] .fa-circle { color: #0275d8; } |
||||
#signed_certificates .filterable[data-state='dead'] .fa-circle { color: #d9534f; } |
||||
|
After Width: | Height: | Size: 393 KiB |
After Width: | Height: | Size: 368 KiB |
After Width: | Height: | Size: 328 KiB |
After Width: | Height: | Size: 139 KiB |
After Width: | Height: | Size: 338 KiB |
After Width: | Height: | Size: 318 KiB |
After Width: | Height: | Size: 342 KiB |
After Width: | Height: | Size: 394 KiB |
After Width: | Height: | Size: 132 KiB |
After Width: | Height: | Size: 502 KiB |
After Width: | Height: | Size: 137 KiB |
After Width: | Height: | Size: 583 KiB |
After Width: | Height: | Size: 638 KiB |
@ -0,0 +1,48 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="utf-8"/> |
||||
<title>Pinecrypt Gateway</title> |
||||
<link href="/css/bundle.css" rel="stylesheet" type="text/css"/> |
||||
<link href="/css/style.css" rel="stylesheet" type="text/css"/> |
||||
<script type="text/javascript" src="/js/bundle.js"></script> |
||||
<script type="text/javascript" src="/js/certidude.js"></script> |
||||
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"> |
||||
</head> |
||||
<body onhashchange="onHashChanged();"> |
||||
<nav class="navbar navbar-toggleable-md navbar-inverse bg-inverse fixed-top"> |
||||
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation"> |
||||
<span class="navbar-toggler-icon"></span> |
||||
</button> |
||||
<a class="navbar-brand" href="#columns=2">Pinecrypt Gateway</a> |
||||
|
||||
<div class="collapse navbar-collapse" id="navbarsExampleDefault"> |
||||
<ul class="navbar-nav mr-auto"> |
||||
<li class="nav-item"> |
||||
<a class="nav-link disabled dashboard" href="#">Dashboard</a> |
||||
</li> |
||||
<li class="nav-item hidden-xl-up"> |
||||
<a class="nav-link" href="#">Log</a> |
||||
</li> |
||||
</ul> |
||||
<div class="form-inline my-2 my-lg-0"> |
||||
<input id="search" class="form-control mr-sm-2" style="display:none;" type="search" placeholder="🔍"> |
||||
</div> |
||||
</div> |
||||
</nav> |
||||
<div id="view-dashboard" class="container-fluid" style="margin: 5em 0 0 0;"> |
||||
<div class="loader-container"> |
||||
<div class="loader"></div> |
||||
<p>Loading certificate authority...</p> |
||||
</div> |
||||
</div> |
||||
<footer class="footer"> |
||||
<div class="container"> |
||||
Pinecrypt Gateway by |
||||
<a href="https://pinecrypt.com">Pinecrypt Labs</a> |
||||
</div> |
||||
</footer> |
||||
</body> |
||||
</html> |
||||
|
@ -0,0 +1,795 @@ |
||||
|
||||
'use strict'; |
||||
|
||||
const KEY_SIZE = 2048; |
||||
const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedora", "Mac", "Linux"]; |
||||
|
||||
jQuery.timeago.settings.allowFuture = true; |
||||
|
||||
function onLaunchShell(common_name) { |
||||
$.post({ |
||||
url: "/api/signed/" + common_name + "/shell/", |
||||
error: function() { |
||||
alert("Failed to launch shell for: " + common_name); |
||||
}, |
||||
success: function(blob) { |
||||
console.info(blob); |
||||
if (blob.action == "start-session") { |
||||
console.info("Returned shell blob:", blob); |
||||
var win = window.open("/shell.html#" + blob.source + "/" + blob.target, '_blank'); |
||||
if (win) { |
||||
win.focus(); |
||||
} else { |
||||
alert("Please disable popup blocker for this site!"); |
||||
} |
||||
} else { |
||||
alert("Failed to start session"); |
||||
} |
||||
} |
||||
}); |
||||
return false; |
||||
} |
||||
|
||||
function onRejectRequest(e, common_name, sha256sum) { |
||||
$(this).button('loading'); |
||||
$.ajax({ |
||||
url: "/api/request/" + common_name + "/?sha256sum=" + sha256sum, |
||||
type: "delete" |
||||
}); |
||||
} |
||||
|
||||
function onSignRequest(e, common_name, sha256sum) { |
||||
e.preventDefault(); |
||||
$(e.target).button('loading'); |
||||
$.ajax({ |
||||
url: "/api/request/" + common_name + "/?sha256sum=" + sha256sum, |
||||
type: "post" |
||||
}); |
||||
return false; |
||||
} |
||||
|
||||
function normalizeCommonName(j) { |
||||
return j.replace("@", "--").split(".").join("-"); // dafuq ?!
|
||||
} |
||||
|
||||
function onShowAll() { |
||||
var options = document.querySelectorAll(".option"); |
||||
for (i = 0; i < options.length; i++) { |
||||
options[i].style.display = "block"; |
||||
} |
||||
} |
||||
|
||||
function onKeyGen() { |
||||
if (window.navigator.userAgent.indexOf(" Edge/") >= 0) { |
||||
$("#enroll .loader-container").hide(); |
||||
$("#enroll .edge-broken").show(); |
||||
return; |
||||
} |
||||
|
||||
window.keys = forge.pki.rsa.generateKeyPair(KEY_SIZE); |
||||
console.info('Key-pair created.'); |
||||
|
||||
window.csr = forge.pki.createCertificationRequest(); |
||||
csr.publicKey = keys.publicKey; |
||||
csr.setSubject([{ |
||||
name: 'commonName', value: common_name |
||||
}]); |
||||
|
||||
csr.sign(keys.privateKey, forge.md.sha384.create()); |
||||
console.info('Certification request created'); |
||||
|
||||
|
||||
$("#enroll .loader-container").hide(); |
||||
|
||||
var prefix = null; |
||||
for (i in DEVICE_KEYWORDS) { |
||||
var keyword = DEVICE_KEYWORDS[i]; |
||||
if (window.navigator.userAgent.indexOf(keyword) >= 0) { |
||||
prefix = keyword.toLowerCase(); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (prefix == null) { |
||||
$(".option").show(); |
||||
return; |
||||
} |
||||
|
||||
var protocols = query.protocols.split(","); |
||||
console.info("Showing snippets for:", protocols); |
||||
for (var j = 0; j < protocols.length; j++) { |
||||
var options = document.querySelectorAll(".option." + protocols[j] + "." + prefix); |
||||
for (i = 0; i < options.length; i++) { |
||||
options[i].style.display = "block"; |
||||
} |
||||
} |
||||
$(".option.any").show(); |
||||
} |
||||
|
||||
function blobToUuid(blob) { |
||||
var md = forge.md.md5.create(); |
||||
md.update(blob); |
||||
var digest = md.digest().toHex(); |
||||
return digest.substring(0, 8) + "-" + |
||||
digest.substring(8, 12) + "-" + |
||||
digest.substring(12, 16) + "-" + |
||||
digest.substring(16,20) + "-" + |
||||
digest.substring(20); |
||||
} |
||||
|
||||
function onEnroll(encoding) { |
||||
console.info("Service name:", query.title); |
||||
|
||||
console.info("User agent:", window.navigator.userAgent); |
||||
var xhr = new XMLHttpRequest(); |
||||
xhr.open('GET', "/api/certificate/"); |
||||
xhr.onload = function() { |
||||
if (xhr.status === 200) { |
||||
var ca = forge.pki.certificateFromPem(xhr.responseText); |
||||
console.info("Got CA certificate:"); |
||||
var xhr2 = new XMLHttpRequest(); |
||||
xhr2.open("PUT", "/api/token/?token=" + query.token ); |
||||
xhr2.onload = function() { |
||||
if (xhr2.status === 200) { |
||||
var a = document.createElement("a"); |
||||
var cert = forge.pki.certificateFromPem(xhr2.responseText); |
||||
console.info("Got signed certificate:", xhr2.responseText); |
||||
var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1( |
||||
keys.privateKey, [cert, ca], "", {algorithm: '3des'})).getBytes(); |
||||
|
||||
switch(encoding) { |
||||
case 'p12': |
||||
var buf = forge.asn1.toDer(p12).getBytes(); |
||||
var mimetype = "application/x-pkcs12" |
||||
a.download = query.title + ".p12"; |
||||
break |
||||
case 'sswan': |
||||
var buf = JSON.stringify({ |
||||
uuid: blobToUuid(authority.namespace), |
||||
name: authority.namespace, |
||||
type: "ikev2-cert", |
||||
'ike-proposal': 'aes256-sha384-prfsha384-modp2048', |
||||
'esp-proposal': 'aes128gcm16-modp2048', |
||||
remote: { |
||||
addr: authority.namespace, |
||||
revocation: { |
||||
crl: false, |
||||
strict: true |
||||
} |
||||
}, |
||||
local: { |
||||
p12: forge.util.encode64(p12) |
||||
} |
||||
}); |
||||
console.info("Buf is:", buf); |
||||
var mimetype = "application/vnd.strongswan.profile" |
||||
a.download = query.title + ".sswan"; |
||||
break |
||||
case 'ovpn': |
||||
var buf = nunjucks.render('snippets/openvpn-client.conf', { |
||||
authority: authority, |
||||
key: forge.pki.privateKeyToPem(keys.privateKey), |
||||
cert: xhr2.responseText, |
||||
ca: xhr.responseText |
||||
}); |
||||
var mimetype = "application/x-openvpn-profile"; |
||||
a.download = query.title + ".ovpn"; |
||||
break |
||||
case 'mobileconfig': |
||||
var p12 = forge.asn1.toDer(forge.pkcs12.toPkcs12Asn1( |
||||
keys.privateKey, [cert, ca], "1234", {algorithm: '3des'})).getBytes(); |
||||
var buf = nunjucks.render('snippets/ios.mobileconfig', { |
||||
authority: authority, |
||||
service_uuid: blobToUuid(query.title), |
||||
conf_uuid: blobToUuid(query.title + " conf1"), |
||||
title: query.title, |
||||
common_name: common_name, |
||||
gateway: authority.namespace, |
||||
p12_uuid: blobToUuid(p12), |
||||
p12: forge.util.encode64(p12), |
||||
ca_uuid: blobToUuid(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes()), |
||||
ca: forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes()) |
||||
}); |
||||
var mimetype = "application/x-apple-aspen-config"; |
||||
a.download = query.title + ".mobileconfig"; |
||||
break |
||||
} |
||||
a.href = "data:" + mimetype + ";base64," + forge.util.encode64(buf); |
||||
console.info("Offering bundle for download"); |
||||
document.body.appendChild(a); // Firefox needs this!
|
||||
a.click(); |
||||
} else { |
||||
if (xhr2.status == 403) { alert("Token used or expired"); } |
||||
console.info('Request failed. Returned status of ' + xhr2.status); |
||||
try { |
||||
var r = JSON.parse(xhr2.responseText); |
||||
console.info("Server said: " + r.title); |
||||
console.info(r.description); |
||||
} catch(e) { |
||||
console.info("Server said: " + xhr2.statusText); |
||||
} |
||||
} |
||||
}; |
||||
xhr2.send(forge.pki.certificationRequestToPem(csr)); |
||||
} |
||||
} |
||||
xhr.send(); |
||||
} |
||||
|
||||
function onHashChanged() { |
||||
|
||||
window.query = {}; |
||||
var a = location.hash.substring(1).split('&'); |
||||
for (var i = 0; i < a.length; i++) { |
||||
var b = a[i].split('='); |
||||
query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || ''); |
||||
} |
||||
|
||||
console.info("Hash is now:", query); |
||||
|
||||
$.get({ |
||||
method: "GET", |
||||
url: "/api/bootstrap/", |
||||
error: function(response) { |
||||
if (response.responseJSON) { |
||||
var msg = response.responseJSON |
||||
} else { |
||||
var msg = { title: "Error " + response.status, description: response.statusText } |
||||
} |
||||
$("#view-dashboard").html(env.render('views/error.html', { message: msg })); |
||||
}, |
||||
success: function(authority) { |
||||
window.authority = authority |
||||
|
||||
// Device identifier
|
||||
var dig = forge.md.sha384.create(); |
||||
dig.update(window.navigator.userAgent); |
||||
|
||||
var prefix = "unknown"; |
||||
for (i in DEVICE_KEYWORDS) { |
||||
var keyword = DEVICE_KEYWORDS[i]; |
||||
if (window.navigator.userAgent.indexOf(keyword) >= 0) { |
||||
prefix = keyword.toLowerCase(); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
window.common_name = prefix + "-" + dig.digest().toHex().substring(0, 5); |
||||
console.info("Device identifier:", common_name); |
||||
|
||||
if (window.location.protocol != "https:") { |
||||
$("#view-dashboard").html(env.render('views/insecure.html', {authority:authority})); |
||||
} else { |
||||
if (query.action == "enroll") { |
||||
$("#view-dashboard").html(env.render('views/enroll.html', { |
||||
common_name: common_name, |
||||
authority: authority, |
||||
token: query.token, |
||||
})); |
||||
var options = document.querySelectorAll(".option"); |
||||
for (i = 0; i < options.length; i++) { |
||||
options[i].style.display = "none"; |
||||
} |
||||
setTimeout(onKeyGen, 100); |
||||
console.info("Generating key pair..."); |
||||
} else { |
||||
loadAuthority(query); |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
|
||||
} |
||||
|
||||
function onTagClicked(e) { |
||||
e.preventDefault(); |
||||
var cn = $(e.target).attr("data-cn"); |
||||
var id = $(e.target).attr("title"); |
||||
var value = $(e.target).html(); |
||||
var updated = prompt("Enter new tag or clear to remove the tag", value); |
||||
if (updated == "") { |
||||
$(event.target).addClass("disabled"); |
||||
$.ajax({ |
||||
method: "DELETE", |
||||
url: "/api/signed/" + cn + "/tag/" + id + "/" |
||||
}); |
||||
} else if (updated && updated != value) { |
||||
$(e.target).addClass("disabled"); |
||||
$.ajax({ |
||||
method: "PUT", |
||||
url: "/api/signed/" + cn + "/tag/" + id + "/", |
||||
data: { value: updated }, |
||||
dataType: "text", |
||||
complete: function(xhr, status) { |
||||
console.info("Tag added successfully", xhr.status, status); |
||||
}, |
||||
success: function() { |
||||
}, |
||||
error: function(xhr, status, e) { |
||||
console.info("Submitting request failed with:", status, e); |
||||
alert(e); |
||||
} |
||||
}); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
function onNewTagClicked(e) { |
||||
e.preventDefault(); |
||||
var cn = $(e.target).attr("data-cn"); |
||||
var key = $(e.target).attr("data-key"); |
||||
var value = prompt("Enter new " + key + " tag for " + cn); |
||||
if (!value) return; |
||||
if (value.length == 0) return; |
||||
var $container = $(".tags[data-cn='" + cn + "']"); |
||||
$container.addClass("disabled"); |
||||
$.ajax({ |
||||
method: "POST", |
||||
url: "/api/signed/" + cn + "/tag/", |
||||
data: { value: value, key: key }, |
||||
dataType: "text", |
||||
complete: function(xhr, status) { |
||||
console.info("Tag added successfully", xhr.status, status); |
||||
}, |
||||
success: function() { |
||||
$container.removeClass("disabled"); |
||||
}, |
||||
error: function(xhr, status, e) { |
||||
console.info("Submitting request failed with:", status, e); |
||||
alert(e); |
||||
} |
||||
}); |
||||
return false; |
||||
} |
||||
|
||||
function onTagFilterChanged() { |
||||
var key = $(event.target).val(); |
||||
console.info("New key is:", key); |
||||
} |
||||
|
||||
function onLogEntry (e) { |
||||
if (e.data) { |
||||
e = JSON.parse(e.data); |
||||
e.fresh = true; |
||||
} |
||||
|
||||
if ($("#log-level-" + e.severity).prop("checked")) { |
||||
$("#log-entries").prepend(env.render("views/logentry.html", { |
||||
entry: { |
||||
created: new Date(e.created).toLocaleString(), |
||||
message: e.message, |
||||
severity: e.severity, |
||||
fresh: e.fresh, |
||||
keywords: e.message.toLowerCase().split(/,?[ <>/]+/).join("|") |
||||
} |
||||
})); |
||||
} |
||||
}; |
||||
|
||||
function onRequestSubmitted(e) { |
||||
console.log("Request submitted:", e.data); |
||||
$.ajax({ |
||||
method: "GET", |
||||
url: "/api/request/id/" + e.data + "/", |
||||
dataType: "json", |
||||
success: function(request, status, xhr) { |
||||
console.info("Going to prepend:", request); |
||||
onRequestDeleted(request.id); // Delete any existing ones just in case
|
||||
$("#pending_requests").prepend( |
||||
env.render('views/request.html', { request: request, authority: authority })); |
||||
$("#pending_requests time").timeago(); |
||||
}, |
||||
error: function(response) { |
||||
console.info("Failed to retrieve certificate:", response); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function onRequestDeleted(e) { |
||||
console.log("Removing deleted request", e.data); |
||||
$("#request-" + e.data).remove(); |
||||
} |
||||
|
||||
function onLeaseUpdate(e) { |
||||
var slug = normalizeCommonName(e.data); |
||||
console.log("Lease updated:", e.data); |
||||
$.ajax({ |
||||
method: "GET", |
||||
url: "/api/signed/" + e.data + "/lease/", |
||||
dataType: "json", |
||||
success: function(lease, status, xhr) { |
||||
console.info("Retrieved lease update details:", lease); |
||||
lease.age = (new Date() - new Date(lease.last_seen)) / 1000.0 |
||||
var $lease = $("#certificate-" + slug + " .lease"); |
||||
$lease.html(env.render('views/lease.html', { |
||||
certificate: { |
||||
lease: lease }})); |
||||
$("time", $lease).timeago(); |
||||
filterSigned(); |
||||
}, |
||||
error: function(response) { |
||||
console.info("Failed to retrieve certificate:", response); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function onRequestSigned(e) { |
||||
console.log("Request signed:", e.data); |
||||
var id = e.data |
||||
//var slug = normalizeCommonName(e.data);
|
||||
//console.log("Removing:", slug);
|
||||
console.log("Removing:", e.data); |
||||
|
||||
$("#request-" + id).slideUp("normal", function() { $(this).remove(); }); |
||||
$("#certificate-" + id).slideUp("normal", function() { $(this).remove(); }); |
||||
|
||||
$.ajax({ |
||||
method: "GET", |
||||
url: "/api/signed/id/" + e.data + "/", |
||||
dataType: "json", |
||||
success: function(certificate, status, xhr) { |
||||
console.info("Retrieved certificate:", certificate); |
||||
$("#signed_certificates").prepend( |
||||
env.render('views/signed.html', { certificate: certificate, session: session })); |
||||
$("#signed_certificates time").timeago(); // TODO: optimize?
|
||||
filterSigned(); |
||||
}, |
||||
error: function(response) { |
||||
console.info("Failed to retrieve certificate:", response); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function onCertificateRevoked(e) { |
||||
console.log("Removing revoked certificate", e.data); |
||||
$("#certificate-" + e.data).slideUp("normal", function() { $(this).remove(); }); |
||||
} |
||||
|
||||
function onTagUpdated(e) { |
||||
var cn = e.data; |
||||
console.log("Tag updated event recevied", cn); |
||||
$.ajax({ |
||||
method: "GET", |
||||
url: "/api/signed/" + cn + "/tag/", |
||||
dataType: "json", |
||||
success:function(tags, status, xhr) { |
||||
console.info("Updated", cn, "tags", tags); |
||||
$(".tags[data-cn='" + cn+"']").html( |
||||
env.render('views/tags.html', { |
||||
certificate: { |
||||
common_name: cn, |
||||
tags:tags }})); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
function onAttributeUpdated(e) { |
||||
var cn = e.data; |
||||
console.log("Attributes updated", cn); |
||||
$.ajax({ |
||||
method: "GET", |
||||
url: "/api/signed/" + cn + "/attr/", |
||||
dataType: "json", |
||||
success:function(attributes, status, xhr) { |
||||
console.info("Updated", cn, "attributes", attributes); |
||||
$(".attributes[data-cn='" + cn + "']").html( |
||||
env.render('views/attributes.html', { |
||||
certificate: { |
||||
common_name: cn, |
||||
attributes:attributes }})); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
function onSubmitRequest() { |
||||
$.ajax({ |
||||
method: "POST", |
||||
url: "/api/request/", |
||||
headers: { |
||||
"Accept": "application/json; charset=utf-8", |
||||
"Content-Type": "application/pkcs10" |
||||
}, |
||||
data: $("#request_body").val(), |
||||
|
||||
success:function(attributes, status, xhr) { |
||||
// Close the modal
|
||||
$("[data-dismiss=modal]").trigger({ type: "click" }); |
||||
}, |
||||
error: function(xhr, status, e) { |
||||
console.info("Submitting request failed with:", status, e); |
||||
alert(e); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
function onServerStarted() { |
||||
console.info("Server started"); |
||||
location.reload(); |
||||
} |
||||
|
||||
function onServerStopped() { |
||||
$("#view-dashboard").html('<div class="loader"></div><p>Server under maintenance</p>'); |
||||
console.info("Server stopped"); |
||||
|
||||
} |
||||
|
||||
function onIssueToken() { |
||||
$.ajax({ |
||||
method: "POST", |
||||
url: "/api/token/", |
||||
data: { username: $("#token_username").val(), mail: $("#token_mail").val() }, |
||||
dataType: "text", |
||||
complete: function(xhr, status) { |
||||
console.info("Token sent successfully", xhr.status, status); |
||||
}, |
||||
success: function(data) { |
||||
var url = JSON.parse(data).url; |
||||
console.info("DATA:", url); |
||||
var code = new QRCode({ |
||||
content: url, |
||||
width: 512, |
||||
height: 512, |
||||
}); |
||||
document.getElementById("token_qrcode").innerHTML = code.svg(); |
||||
|
||||
}, |
||||
error: function(xhr, status, e) { |
||||
console.info("Submitting request failed with:", status, e); |
||||
alert(e); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function filterSigned() { |
||||
if ($("#search").val() != "") { |
||||
console.info("Not filtering by state since keyword filter is active"); |
||||
return; |
||||
} |
||||
|
||||
$("#signed_certificates .filterable").each(function(i,j) { |
||||
var last_seen = j.getAttribute("data-last-seen"); |
||||
var state = "new"; |
||||
if (last_seen) { |
||||
var age = (new Date() - new Date(last_seen)) / 1000; |
||||
if (age > 172800) { |
||||
state = "dead"; |
||||
} else if (age > 10800) { |
||||
state = "offline"; |
||||
} else { |
||||
state = "online"; |
||||
} |
||||
} |
||||
j.setAttribute("data-state", state); |
||||
j.style.display = $("#signed-filter-" + state).prop("checked") ? "block" : "none"; |
||||
}); |
||||
$("#signed-total").html($("#signed_certificates .filterable").length); |
||||
$("#signed-filter-counter").html($("#signed_certificates .filterable:visible").length); |
||||
} |
||||
|
||||
function loadAuthority(query) { |
||||
console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'"); |
||||
$.ajax({ |
||||
method: "GET", |
||||
url: "/api/session/", |
||||
dataType: "json", |
||||
error: function(response) { |
||||
if (response.responseJSON) { |
||||
var msg = response.responseJSON |
||||
} else { |
||||
var msg = { title: "Error " + response.status, description: response.statusText } |
||||
} |
||||
$("#view-dashboard").html(env.render('views/error.html', { message: msg })); |
||||
}, |
||||
success: function(session, status, xhr) { |
||||
window.session = session; |
||||
|
||||
console.info("Loaded:", session); |
||||
$("#login").hide(); |
||||
$("#search").show(); |
||||
|
||||
/** |
||||
* Render authority views |
||||
**/ |
||||
$("#view-dashboard").html(env.render('views/authority.html', { |
||||
session: session, |
||||
authority: authority, |
||||
window: window, |
||||
|
||||
// Parameters for unified snippets
|
||||
dhparam_path: "/etc/ssl/dhparam.pem", |
||||
key_path: "/etc/certidude/authority/" + window.authority.namespace + "/host_key.pem", |
||||
certificate_path: "/etc/certidude/authority/" + window.authority.namespace + "/host_cert.pem", |
||||
authority_path: "/etc/certidude/authority/" + window.authority.namespace + "/ca_cert.pem", |
||||
revocations_path: "/etc/certidude/authority/" + window.authority.namespace + "/crl.pem", |
||||
common_name: "$NAME" |
||||
})); |
||||
|
||||
// Initial filtering
|
||||
$("#signed-filter .btn").on('change', filterSigned); |
||||
filterSigned(); |
||||
|
||||
// Attach timeago events
|
||||
$("time").timeago(); |
||||
|
||||
if (window.authority) { |
||||
$("#log input").each(function(i, e) { |
||||
console.info("e.checked:", e.checked , "and", e.id, "@localstorage is", localStorage[e.id], "setting to:", localStorage[e.id] || e.checked, "bool:", localStorage[e.id] || e.checked == "true"); |
||||
e.checked = localStorage[e.id] ? localStorage[e.id] == "true" : e.checked; |
||||
}); |
||||
|
||||
$("#log input").change(function() { |
||||
localStorage[this.id] = this.checked; |
||||
}); |
||||
|
||||
console.info("Opening EventSource from:", window.session.events); |
||||
|
||||
window.source = new EventSource(window.session.events); |
||||
|
||||
source.onmessage = function(event) { |
||||
console.log("Received server-sent event:", event); |
||||
} |
||||
|
||||
|
||||
source.addEventListener("lease-update", onLeaseUpdate); |
||||
source.addEventListener("request-deleted", onRequestDeleted); |
||||
source.addEventListener("request-submitted", onRequestSubmitted); |
||||
source.addEventListener("request-signed", onRequestSigned); |
||||
source.addEventListener("certificate-revoked", onCertificateRevoked); |
||||
source.addEventListener("tag-update", onTagUpdated); |
||||
source.addEventListener("attribute-update", onAttributeUpdated); |
||||
source.addEventListener("server-started", onServerStarted); |
||||
source.addEventListener("server-stopped", onServerStopped); |
||||
|
||||
} |
||||
|
||||
$("nav#menu li").click(function(e) { |
||||
$("section").hide(); |
||||
$("section#" + $(e.target).attr("data-section")).show(); |
||||
}); |
||||
|
||||
|
||||
|
||||
$("#enroll").click(function() { |
||||
var keys = forge.pki.rsa.generateKeyPair(1024); |
||||
|
||||
$.ajax({ |
||||
method: "POST", |
||||
url: "/api/token/", |
||||
data: "username=" + session.user.name, |
||||
complete: function(xhr, status) { |
||||
console.info("Token generated successfully:", xhr, status); |
||||
|
||||
}, |
||||
error: function(xhr, status, e) { |
||||
console.info("Token generation failed:", status, e); |
||||
alert(e); |
||||
} |
||||
}); |
||||
|
||||
|
||||
|
||||
var privateKeyBuffer = forge.pki.privateKeyToPem(keys.privateKey); |
||||
}); |
||||
|
||||
/** |
||||
* Set up search bar |
||||
*/ |
||||
$(window).on("search", function() { |
||||
var q = $("#search").val(); |
||||
$(".filterable").each(function(i, e) { |
||||
if ($(e).attr("data-keywords").toLowerCase().indexOf(q) >= 0) { |
||||
$(e).show(); |
||||
} else { |
||||
$(e).hide(); |
||||
} |
||||
}); |
||||
if (q.length == 0) { |
||||
filterSigned(); |
||||
$("#signed-filter .btn").removeClass("disabled").prop("disabled", false); |
||||
} else { |
||||
$("#signed-filter .btn").addClass("disabled").prop("disabled", true); |
||||
} |
||||
}); |
||||
|
||||
|
||||
|
||||
|
||||
/** |
||||
* Bind key up event of search bar |
||||
*/ |
||||
$("#search").on("keyup", function() { |
||||
if (window.searchTimeout) { clearTimeout(window.searchTimeout); } |
||||
window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500); |
||||
}); |
||||
|
||||
if (session.request_submission_allowed) { |
||||
$("#request_submit").click(function() { |
||||
$(this).addClass("busy"); |
||||
$.ajax({ |
||||
method: "POST", |
||||
contentType: "application/pkcs10", |
||||
url: "/api/request/", |
||||
data: $("#request_body").val(), |
||||
dataType: "text", |
||||
complete: function(xhr, status) { |
||||
console.info("Request submitted successfully, server returned", xhr.status, status); |
||||
$("#request_submit").removeClass("busy"); |
||||
}, |
||||
success: function() { |
||||
// Clear textarea on success
|
||||
$("#request_body").val(""); |
||||
}, |
||||
error: function(xhr, status, e) { |
||||
console.info("Submitting request failed with:", status, e); |
||||
alert(e); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
$("nav .nav-link.dashboard").removeClass("disabled").click(function() { |
||||
$("#column-requests").show(); |
||||
$("#column-signed").show(); |
||||
$("#column-revoked").show(); |
||||
$("#column-log").hide(); |
||||
}); |
||||
|
||||
/** |
||||
* Fetch log entries |
||||
*/ |
||||
if (session.features.logging) { |
||||
if ($("#column-log:visible").length) { |
||||
loadLog(); |
||||
} |
||||
$("nav .nav-link.log").removeClass("disabled").click(function() { |
||||
loadLog(); |
||||
$("#column-requests").show(); |
||||
$("#column-signed").show(); |
||||
$("#column-revoked").show(); |
||||
$("#column-log").hide(); |
||||
}); |
||||
} else { |
||||
console.info("Log disabled"); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function loadLog() { |
||||
if (window.log_initialized) { |
||||
console.info("Log already loaded"); |
||||
return; |
||||
} |
||||
console.info("Loading log..."); |
||||
window.log_initialized = true; |
||||
$.ajax({ |
||||
method: "GET", |
||||
url: "/api/log/?limit=100", |
||||
dataType: "json", |
||||
success: function(entries, status, xhr) { |
||||
console.info("Got", entries.length, "log entries"); |
||||
for (var j = entries.length-1; j--; ) { |
||||
onLogEntry(entries[j]); |
||||
}; |
||||
source.addEventListener("log-entry", onLogEntry); |
||||
$("#column-log .loader-container").hide(); |
||||
$("#column-log .content").show(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function datetimeFilter(s) { |
||||
return new Date(s); |
||||
} |
||||
|
||||
function serialFilter(s) { |
||||
return s.substring(0,s.length-14) + " " + |
||||
s.substring(s.length-14); |
||||
} |
||||
|
||||
$(document).ready(function() { |
||||
window.env = new nunjucks.Environment(); |
||||
env.addFilter("datetime", datetimeFilter); |
||||
env.addFilter("serial", serialFilter); |
||||
onHashChanged(); |
||||
}); |
@ -0,0 +1,104 @@ |
||||
function onChooseFile(event) { |
||||
if (!(window.File && window.FileReader && window.FileList && window.Blob)) { |
||||
alert('The File APIs are not fully supported in this browser.'); |
||||
} |
||||
|
||||
var files = event.target.files; |
||||
|
||||
|
||||
var reader = new FileReader(); |
||||
|
||||
// Closure to capture the file information.
|
||||
reader.onload = (function(theFile) { |
||||
return function(e) { |
||||
// Render thumbnail.
|
||||
var span = document.createElement('span'); |
||||
span.innerHTML = ['<img class="thumb" src="', e.target.result, |
||||