├── .dockerignore ├── .gitattributes ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config └── config.exs ├── frontend ├── .babelrc ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .postcssrc.js ├── Dockerfile ├── Makefile ├── README.md ├── build │ ├── build.js │ ├── check-versions.js │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ └── prod.env.js ├── dist │ ├── index.html │ └── static │ │ ├── css │ │ ├── app.css │ │ └── app.css.map │ │ ├── favicon.png │ │ ├── fonts │ │ ├── devicon.eot │ │ ├── devicon.ttf │ │ ├── devicon.woff │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 │ │ ├── img │ │ ├── certstream-overview.png │ │ ├── devicon.svg │ │ ├── doghead.png │ │ ├── fa-brands-400.svg │ │ └── fa-solid-900.svg │ │ └── js │ │ ├── app.47135fa.js │ │ ├── app.47135fa.js.map │ │ ├── manifest.47135fa.js │ │ ├── manifest.47135fa.js.map │ │ ├── vendor.47135fa.js │ │ └── vendor.47135fa.js.map ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.vue │ ├── assets │ │ ├── .gitkeep │ │ ├── devicon-colors.css │ │ ├── devicon.css │ │ ├── example.json │ │ ├── fonts │ │ │ ├── devicon.eot │ │ │ ├── devicon.svg │ │ │ ├── devicon.ttf │ │ │ └── devicon.woff │ │ └── img │ │ │ ├── certstream-bg.png │ │ │ ├── certstream-overview.png │ │ │ ├── doghead.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ └── rolling-transition.png │ ├── components │ │ ├── FeedWatcher.vue │ │ └── Frontpage.vue │ ├── main.js │ └── router │ │ └── index.js └── static │ ├── .gitkeep │ └── favicon.png ├── lib ├── certstream.ex └── certstream │ ├── certificate_buffer_agent.ex │ ├── client_manager_agent.ex │ ├── ct_parser.ex │ ├── ct_watcher.ex │ └── web.ex ├── mix.exs ├── mix.lock └── test ├── cert_buffer_agent_test.exs ├── ctl_parser_test.exs └── test_helper.exs /.dockerignore: -------------------------------------------------------------------------------- 1 | # Mix artifacts 2 | _build 3 | deps 4 | *.ez 5 | 6 | # IDE shit 7 | .idea 8 | *.iml 9 | 10 | # Misc 11 | README.md 12 | 13 | html/node_modules/ -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | frontend/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | .elixir_ls/ 23 | 24 | .idea/ 25 | .idea/vcs.xml 26 | 27 | *.iml 28 | 29 | frontend/node_modules 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM elixir:1.8-alpine 2 | 3 | WORKDIR /opt/app 4 | 5 | ENV HOME /opt/app 6 | ENV MIX_HOME=/opt/mix 7 | ENV HEX_HOME=/opt/hex 8 | ENV MIX_ENV=prod 9 | 10 | RUN apk add git 11 | 12 | RUN mix local.hex --force && mix local.rebar --force 13 | 14 | ADD mix.exs ./ 15 | ADD mix.lock ./ 16 | 17 | RUN mix do deps.get, deps.compile 18 | 19 | COPY frontend/dist/ /opt/app/frontend/dist/ 20 | COPY config/ /opt/app/config/ 21 | 22 | COPY lib /opt/app/lib/ 23 | 24 | CMD mix run --no-halt 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cali Dog Security 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

CertStream-Server

4 |

Aggregate and broadcast SSL certs as they're issued live.

5 |

6 | 7 | **Certstream-server** is a service written in elixir to aggregate, parse, and stream certificate data from multiple [certificate transparency logs](https://www.certificate-transparency.org/what-is-ct). It leverages the amazing power of elixir/erlang to achieve great network throughput and concurrency with very little resource usage. 8 | 9 | This is a rewrite of the [original version written in python](https://github.com/CaliDog/certstream-server-python), and is much more efficient than the original and currently ships millions of certificates a day on a single Hetzner dedicated server without issue (\~250TB of data every month!). 10 | 11 | ## Getting Started 12 | 13 | Getting up and running is pretty easy (especially if you use Heroku, as we include a Dockerfile!). 14 | 15 | First install elixir (assuming you're on OSX, otherwise follow [instructions for your platform](https://elixir-lang.org/install.html)) 16 | 17 | ``` 18 | $ brew install elixir 19 | ``` 20 | 21 | Then fetch the dependencies 22 | 23 | ``` 24 | $ mix deps.get 25 | ``` 26 | 27 | From there you can run it 28 | 29 | ``` 30 | $ mix run --no-halt 31 | ``` 32 | 33 | Alternatively, you can run it in an iex session (so you can interact with it while it's running) for development 34 | 35 | ``` 36 | $ iex -S mix 37 | Interactive Elixir (1.8.2) - press Ctrl+C to exit (type h() ENTER for help) 38 | iex(1)> :observer.start 39 | ``` 40 | 41 | This will open up an http/websocket server on port 4000 (override this by setting a `PORT` environment variable). 42 | 43 | ## Usage 44 | 45 | Once you have the server runing on some port, you can visit the server to be greeted with the index page. 46 | 47 | If you point a websocket client at the root URL (`/`), it'll give you a data structure that doesn't bundle the DER-encoded certificate. 48 | 49 | If you want the certificate data as well, you can point a websocket client to `/full-stream` and it'll also bundle the DER-encoded certificate along with each certificate. 50 | 51 | ## Internals 52 | 53 | The structure of the application is pretty simple. The dataflow basically looks like this: 54 | 55 | ``` 56 | CertificateBuffer 57 | | 58 | HTTP Watcher \ | / Websocket Connection Process 59 | HTTP Watcher - ClientManager - Websocket Connection Process 60 | HTTP Watcher / \ Websocket Connection Process 61 | 62 | ``` 63 | ### HTTP Watchers 64 | First we spin up 1 process for every entry in the [known CTL list from the CT project](https://www.gstatic.com/ct/log_list/v3/all_logs_list.json), with each of them being responsible for sleeping for 10 seconds and checking to see if the top of the merkle tree has changed. Once a difference has been found, they go out and download the certificate data, parsing it and coercing it to a hashmap structure using [the EasySSL library](https://github.com/CaliDog/EasySSL) and sending it to the ClientManager. 65 | 66 | ### ClientManager 67 | This agent is responsible for brokering communication between the CT watchers and the currently connected websocket clients. Certificates are broadcast to websocket connection processes through an erlang [pobox](https://github.com/ferd/pobox) in order to properly load-shed when a slow client isn't reading certificates fast enough. The ClientManager also sends a copy of every certificate received to the CertificateBuffer. 68 | 69 | ### CertificateBuffer 70 | This agent is responsible for keeping a ring-buffer in memory of the most recently seen 25 certificates, as well as counting the certificates processed by Certstream. 71 | 72 | ### Websocket Connection Process 73 | Under the hood we use the erlang library [Cowboy](https://github.com/ninenines/cowboy) to handle static content serving, the json APIs, and websocket connections. There's nothing too special about them other than they're assigned a paired pobox at the start of every connection. 74 | 75 | ## HTTP Routes 76 | 77 | `/latest.json` - Get the most recent 25 certificates CertStream has seen 78 | 79 | `/example.json` - Get the most recent certificate CertStream has seen 80 | 81 | ## Data Structure 82 | 83 | **Certificate Updates** 84 | 85 | ``` 86 | { 87 | "message_type": "certificate_update", 88 | "data": { 89 | "update_type": "X509LogEntry", 90 | "leaf_cert": { 91 | "subject": { 92 | "aggregated": "/CN=e-zigarette-liquid-shop.de", 93 | "C": null, 94 | "ST": null, 95 | "L": null, 96 | "O": null, 97 | "OU": null, 98 | "CN": "e-zigarette-liquid-shop.de" 99 | }, 100 | "extensions": { 101 | "keyUsage": "Digital Signature, Key Encipherment", 102 | "extendedKeyUsage": "TLS Web Server Authentication, TLS Web Client Authentication", 103 | "basicConstraints": "CA:FALSE", 104 | "subjectKeyIdentifier": "AC:4C:7B:3C:E9:C8:7F:CB:E2:7D:5D:64:F2:25:0C:89:C2:AE:F0:5E", 105 | "authorityKeyIdentifier": "keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1\n", 106 | "authorityInfoAccess": "OCSP - URI:http://ocsp.int-x3.letsencrypt.org\nCA Issuers - URI:http://cert.int-x3.letsencrypt.org/\n", 107 | "subjectAltName": "DNS:e-zigarette-liquid-shop.de, DNS:www.e-zigarette-liquid-shop.de", 108 | "certificatePolicies": "Policy: 2.23.140.1.2.1\nPolicy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.letsencrypt.org\n User Notice:\n Explicit Text: This Certificate may only be relied upon by Relying Parties and only in accordance with the Certificate Policy found at https://letsencrypt.org/repository/\n" 109 | }, 110 | "not_before": 1508123861.0, 111 | "not_after": 1515899861.0, 112 | "as_der": "::BASE64_DER_CERT::", 113 | "all_domains": [ 114 | "e-zigarette-liquid-shop.de", 115 | "www.e-zigarette-liquid-shop.de" 116 | ] 117 | }, 118 | "chain": [ 119 | { 120 | "subject": { 121 | "aggregated": "/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3", 122 | "C": "US", 123 | "ST": null, 124 | "L": null, 125 | "O": "Let's Encrypt", 126 | "OU": null, 127 | "CN": "Let's Encrypt Authority X3" 128 | }, 129 | "extensions": { 130 | "basicConstraints": "CA:TRUE, pathlen:0", 131 | "keyUsage": "Digital Signature, Certificate Sign, CRL Sign", 132 | "authorityInfoAccess": "OCSP - URI:http://isrg.trustid.ocsp.identrust.com\nCA Issuers - URI:http://apps.identrust.com/roots/dstrootcax3.p7c\n", 133 | "authorityKeyIdentifier": "keyid:C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10\n", 134 | "certificatePolicies": "Policy: 2.23.140.1.2.1\nPolicy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.root-x1.letsencrypt.org\n", 135 | "crlDistributionPoints": "\nFull Name:\n URI:http://crl.identrust.com/DSTROOTCAX3CRL.crl\n", 136 | "subjectKeyIdentifier": "A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1" 137 | }, 138 | "not_before": 1458232846.0, 139 | "not_after": 1615999246.0, 140 | "as_der": "::BASE64_DER_CERT::" 141 | }, 142 | { 143 | "subject": { 144 | "aggregated": "/O=Digital Signature Trust Co./CN=DST Root CA X3", 145 | "C": null, 146 | "ST": null, 147 | "L": null, 148 | "O": "Digital Signature Trust Co.", 149 | "OU": null, 150 | "CN": "DST Root CA X3" 151 | }, 152 | "extensions": { 153 | "basicConstraints": "CA:TRUE", 154 | "keyUsage": "Certificate Sign, CRL Sign", 155 | "subjectKeyIdentifier": "C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10" 156 | }, 157 | "not_before": 970348339.0, 158 | "not_after": 1633010475.0, 159 | "as_der": "::BASE64_DER_CERT::" 160 | } 161 | ], 162 | "cert_index": 19587936, 163 | "seen": 1508483726.8601687, 164 | "source": { 165 | "url": "mammoth.ct.comodo.com", 166 | "name": "Comodo 'Mammoth' CT log" 167 | } 168 | } 169 | } 170 | ``` 171 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :certstream, 4 | user_agent: :default, # Defaults to "Certstream Server v{CURRENT_VERSION}" 5 | full_stream_url: "/full-stream", 6 | domains_only_url: "/domains-only" 7 | 8 | config :logger, 9 | level: String.to_atom(System.get_env("LOG_LEVEL") || "info"), 10 | backends: [:console] 11 | 12 | config :honeybadger, 13 | app: :certstream, 14 | exclude_envs: [:test], 15 | environment_name: :prod, 16 | use_logger: true 17 | 18 | # Disable connection pooling for HTTP requests 19 | config :hackney, use_default_pool: false 20 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | dist/ 4 | 5 | .DS_Store -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.js] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | amd: true 11 | }, 12 | extends: [ 13 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 14 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 15 | 'plugin:vue/essential', 16 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 17 | 'standard' 18 | ], 19 | // required to lint *.vue files 20 | plugins: [ 21 | 'vue' 22 | ], 23 | // add your custom rules here 24 | rules: { 25 | // allow async-await 26 | 'generator-star-spacing': 'off', 27 | // allow debugger during development 28 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json package-lock.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . -------------------------------------------------------------------------------- /frontend/Makefile: -------------------------------------------------------------------------------- 1 | build-frontend: 2 | if [ -d "dist/" ]; then rm -rf dist/; fi 3 | docker build -t certstream-frontend . 4 | docker run --rm -it -v $$(pwd)/dist/:/app/dist/ certstream-frontend npm run build 5 | docker rmi certstream-frontend -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | To build the frontend without installing npm or dealing with JS, just run `make`! 🚀 2 | 3 | If you want to do development work you can use `npm run dev` which will setup webpack to watch the frontend files and automatically rebuild when they change. You can run the server as usual and changes should reflect (almost) immediately. -------------------------------------------------------------------------------- /frontend/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /frontend/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /frontend/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /frontend/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | const createLintingRule = () => ({ 12 | test: /\.(js|vue)$/, 13 | loader: 'eslint-loader', 14 | enforce: 'pre', 15 | include: [resolve('src'), resolve('test')], 16 | options: { 17 | formatter: require('eslint-friendly-formatter'), 18 | emitWarning: !config.dev.showEslintErrorsInOverlay 19 | } 20 | }) 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './src/main.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | '@': resolve('src'), 38 | } 39 | }, 40 | module: { 41 | rules: [ 42 | ...(config.dev.useEslint ? [createLintingRule()] : []), 43 | { 44 | test: /\.vue$/, 45 | loader: 'vue-loader', 46 | options: vueLoaderConfig 47 | }, 48 | { 49 | test: /\.js$/, 50 | loader: 'babel-loader', 51 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 52 | }, 53 | { 54 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 55 | loader: 'url-loader', 56 | options: { 57 | limit: 10000, 58 | name: utils.assetsPath('img/[name].[ext]') 59 | } 60 | }, 61 | { 62 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 63 | loader: 'url-loader', 64 | options: { 65 | limit: 10000, 66 | name: utils.assetsPath('media/[name].[ext]') 67 | } 68 | }, 69 | { 70 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 71 | loader: 'url-loader', 72 | options: { 73 | limit: 10000, 74 | name: utils.assetsPath('fonts/[name].[ext]') 75 | } 76 | } 77 | ] 78 | }, 79 | node: { 80 | // prevent webpack from injecting useless setImmediate polyfill because Vue 81 | // source contains it (although only uses it if it's native). 82 | setImmediate: false, 83 | // prevent webpack from injecting mocks to Node native modules 84 | // that does not make sense for the client 85 | dgram: 'empty', 86 | fs: 'empty', 87 | net: 'empty', 88 | tls: 'empty', 89 | child_process: 'empty' 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /frontend/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | output: { 24 | path: config.build.assetsRoot, 25 | filename: utils.assetsPath('js/[name].[hash].js'), 26 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'), 27 | }, 28 | plugins: [ 29 | new webpack.DefinePlugin({ 30 | 'process.env': require('../config/dev.env') 31 | }), 32 | // new webpack.HotModuleReplacementPlugin(), 33 | // new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 34 | new webpack.NoEmitOnErrorsPlugin(), 35 | // https://github.com/ampedandwired/html-webpack-plugin 36 | new HtmlWebpackPlugin({ 37 | filename: 'index.html', 38 | template: 'index.html', 39 | inject: true 40 | }), 41 | // copy custom static assets 42 | new CopyWebpackPlugin([ 43 | { 44 | from: path.resolve(__dirname, '../static'), 45 | to: config.dev.assetsSubDirectory, 46 | ignore: ['.*'] 47 | } 48 | ]) 49 | ] 50 | }) 51 | 52 | module.exports = new Promise((resolve, reject) => { 53 | portfinder.basePort = process.env.PORT || config.dev.port 54 | portfinder.getPort((err, port) => { 55 | if (err) { 56 | reject(err) 57 | } else { 58 | // publish the new Port, necessary for e2e tests 59 | process.env.PORT = port 60 | // add port to devServer config 61 | // devWebpackConfig.devServer.port = port 62 | 63 | // Add FriendlyErrorsPlugin 64 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 65 | compilationSuccessInfo: { 66 | }, 67 | onErrors: config.dev.notifyOnErrors 68 | ? utils.createNotifierCallback() 69 | : undefined 70 | })) 71 | 72 | resolve(devWebpackConfig) 73 | } 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /frontend/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = require('../config/prod.env') 15 | 16 | const webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true, 21 | usePostCSS: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].[hash:7].js'), 28 | chunkFilename: utils.assetsPath('js/[id].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new UglifyJsPlugin({ 36 | uglifyOptions: { 37 | compress: { 38 | warnings: false 39 | } 40 | }, 41 | sourceMap: config.build.productionSourceMap, 42 | parallel: true 43 | }), 44 | // extract css into its own file 45 | new ExtractTextPlugin({ 46 | filename: utils.assetsPath('css/[name].css'), 47 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 51 | allChunks: true, 52 | }), 53 | // Compress extracted CSS. We are using this plugin so that possible 54 | // duplicated CSS from different components can be deduped. 55 | new OptimizeCSSPlugin({ 56 | cssProcessorOptions: config.build.productionSourceMap 57 | ? { safe: true, map: { inline: false } } 58 | : { safe: true } 59 | }), 60 | // generate dist index.html with correct asset hash for caching. 61 | // you can customize output by editing /index.html 62 | // see https://github.com/ampedandwired/html-webpack-plugin 63 | new HtmlWebpackPlugin({ 64 | filename: config.build.index, 65 | template: 'index.html', 66 | inject: true, 67 | minify: { 68 | removeComments: true, 69 | collapseWhitespace: true, 70 | removeAttributeQuotes: true 71 | // more options: 72 | // https://github.com/kangax/html-minifier#options-quick-reference 73 | }, 74 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 75 | chunksSortMode: 'dependency' 76 | }), 77 | // keep module.id stable when vendor modules does not change 78 | new webpack.HashedModuleIdsPlugin(), 79 | // enable scope hoisting 80 | new webpack.optimize.ModuleConcatenationPlugin(), 81 | // split vendor js into its own file 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'vendor', 84 | minChunks (module) { 85 | // any required modules inside node_modules are extracted to vendor 86 | return ( 87 | module.resource && 88 | /\.js$/.test(module.resource) && 89 | module.resource.indexOf( 90 | path.join(__dirname, '../node_modules') 91 | ) === 0 92 | ) 93 | } 94 | }), 95 | // extract webpack runtime and module manifest to its own file in order to 96 | // prevent vendor hash from being updated whenever app bundle is updated 97 | new webpack.optimize.CommonsChunkPlugin({ 98 | name: 'manifest', 99 | minChunks: Infinity 100 | }), 101 | // This instance extracts shared chunks from code splitted chunks and bundles them 102 | // in a separate chunk, similar to the vendor chunk 103 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 104 | new webpack.optimize.CommonsChunkPlugin({ 105 | name: 'app', 106 | async: 'vendor-async', 107 | children: true, 108 | minChunks: 3 109 | }), 110 | 111 | // copy custom static assets 112 | new CopyWebpackPlugin([ 113 | { 114 | from: path.resolve(__dirname, '../static'), 115 | to: config.build.assetsSubDirectory, 116 | ignore: ['.*'] 117 | } 118 | ]) 119 | ] 120 | }) 121 | 122 | if (config.build.productionGzip) { 123 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 124 | 125 | webpackConfig.plugins.push( 126 | new CompressionWebpackPlugin({ 127 | asset: '[path].gz[query]', 128 | algorithm: 'gzip', 129 | test: new RegExp( 130 | '\\.(' + 131 | config.build.productionGzipExtensions.join('|') + 132 | ')$' 133 | ), 134 | threshold: 10240, 135 | minRatio: 0.8 136 | }) 137 | ) 138 | } 139 | 140 | if (config.build.bundleAnalyzerReport) { 141 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 142 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 143 | } 144 | 145 | module.exports = webpackConfig 146 | -------------------------------------------------------------------------------- /frontend/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'cheap-module-eval-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | cssSourceMap: true 44 | }, 45 | 46 | build: { 47 | // Template for index.html 48 | index: path.resolve(__dirname, '../dist/index.html'), 49 | 50 | // Paths 51 | assetsRoot: path.resolve(__dirname, '../dist'), 52 | assetsSubDirectory: 'static', 53 | assetsPublicPath: '/', 54 | 55 | /** 56 | * Source Maps 57 | */ 58 | 59 | productionSourceMap: true, 60 | // https://webpack.js.org/configuration/devtool/#production 61 | devtool: '#source-map', 62 | 63 | // Gzip off by default as many popular static hosts such as 64 | // Surge or Netlify already gzip all static assets for you. 65 | // Before setting to `true`, make sure to: 66 | // npm install --save-dev compression-webpack-plugin 67 | productionGzip: false, 68 | productionGzipExtensions: ['js', 'css'], 69 | 70 | // Run the build command with an extra argument to 71 | // View the bundle analyzer report after build finishes: 72 | // `npm run build --report` 73 | // Set to `true` or `false` to always turn it on or off 74 | bundleAnalyzerReport: process.env.npm_config_report 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /frontend/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"', 4 | build: { 5 | productionSourceMap: false, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/dist/index.html: -------------------------------------------------------------------------------- 1 | Certstream
-------------------------------------------------------------------------------- /frontend/dist/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/favicon.png -------------------------------------------------------------------------------- /frontend/dist/static/fonts/devicon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/fonts/devicon.eot -------------------------------------------------------------------------------- /frontend/dist/static/fonts/devicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/fonts/devicon.ttf -------------------------------------------------------------------------------- /frontend/dist/static/fonts/devicon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/fonts/devicon.woff -------------------------------------------------------------------------------- /frontend/dist/static/fonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/fonts/fa-brands-400.eot -------------------------------------------------------------------------------- /frontend/dist/static/fonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/fonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /frontend/dist/static/fonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/fonts/fa-brands-400.woff -------------------------------------------------------------------------------- /frontend/dist/static/fonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/fonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /frontend/dist/static/fonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/fonts/fa-solid-900.eot -------------------------------------------------------------------------------- /frontend/dist/static/fonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/fonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /frontend/dist/static/fonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/fonts/fa-solid-900.woff -------------------------------------------------------------------------------- /frontend/dist/static/fonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/fonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /frontend/dist/static/img/certstream-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/img/certstream-overview.png -------------------------------------------------------------------------------- /frontend/dist/static/img/doghead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/dist/static/img/doghead.png -------------------------------------------------------------------------------- /frontend/dist/static/js/app.47135fa.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([1],{"2+D3":function(t,e,s){"use strict";var a={render:function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("span",{staticClass:"json-tree",class:{"json-tree-root":0===t.parsed.depth}},[t.parsed.primitive?s("span",{staticClass:"json-tree-row"},[t._l(2*t.parsed.depth+3,function(e){return s("span",{key:e,staticClass:"json-tree-indent"},[t._v(" ")])}),t._v(" "),t.parsed.key?s("span",{staticClass:"json-tree-key"},[t._v(t._s(t.parsed.key))]):t._e(),t._v(" "),t.parsed.key?s("span",{staticClass:"json-tree-colon"},[t._v(": ")]):t._e(),t._v(" "),s("span",{staticClass:"json-tree-value",class:"json-tree-value-"+t.parsed.type,attrs:{title:""+t.parsed.value}},[t._v(t._s(""+t.parsed.value))]),t._v(" "),t.parsed.last?t._e():s("span",{staticClass:"json-tree-comma"},[t._v(",")]),t._v(" "),s("span",{staticClass:"json-tree-indent"},[t._v(" ")])],2):t._e(),t._v(" "),t.parsed.primitive?t._e():s("span",{staticClass:"json-tree-deep"},[s("span",{staticClass:"json-tree-row json-tree-expando",on:{click:function(e){t.expanded=!t.expanded},mouseover:function(e){t.hovered=!0},mouseout:function(e){t.hovered=!1}}},[s("span",{staticClass:"json-tree-indent"},[t._v(" ")]),t._v(" "),s("span",{staticClass:"json-tree-sign"},[t._v(t._s(t.expanded?"-":"+"))]),t._v(" "),t._l(2*t.parsed.depth+1,function(e){return s("span",{key:e,staticClass:"json-tree-indent"},[t._v(" ")])}),t._v(" "),t.parsed.key?s("span",{staticClass:"json-tree-key"},[t._v(t._s(t.parsed.key))]):t._e(),t._v(" "),t.parsed.key?s("span",{staticClass:"json-tree-colon"},[t._v(": ")]):t._e(),t._v(" "),s("span",{staticClass:"json-tree-open"},[t._v(t._s("array"===t.parsed.type?"[":"{"))]),t._v(" "),s("span",{directives:[{name:"show",rawName:"v-show",value:!t.expanded,expression:"!expanded"}],staticClass:"json-tree-collapsed"},[t._v(" /* "+t._s(t.format(t.parsed.value.length))+" */ ")]),t._v(" "),s("span",{directives:[{name:"show",rawName:"v-show",value:!t.expanded,expression:"!expanded"}],staticClass:"json-tree-close"},[t._v(t._s("array"===t.parsed.type?"]":"}"))]),t._v(" "),s("span",{directives:[{name:"show",rawName:"v-show",value:!t.expanded&&!t.parsed.last,expression:"!expanded && !parsed.last"}],staticClass:"json-tree-comma"},[t._v(",")]),t._v(" "),s("span",{staticClass:"json-tree-indent"},[t._v(" ")])],2),t._v(" "),s("span",{directives:[{name:"show",rawName:"v-show",value:t.expanded,expression:"expanded"}],staticClass:"json-tree-deeper"},t._l(t.parsed.value,function(e,a){return s("json-tree",{key:a,attrs:{kv:e,level:t.level}})}),1),t._v(" "),s("span",{directives:[{name:"show",rawName:"v-show",value:t.expanded,expression:"expanded"}],staticClass:"json-tree-row"},[s("span",{staticClass:"json-tree-ending",class:{"json-tree-paired":t.hovered}},[t._l(2*t.parsed.depth+3,function(e){return s("span",{key:e,staticClass:"json-tree-indent"},[t._v(" ")])}),t._v(" "),s("span",{staticClass:"json-tree-close"},[t._v(t._s("array"===t.parsed.type?"]":"}"))]),t._v(" "),t.parsed.last?t._e():s("span",{staticClass:"json-tree-comma"},[t._v(",")]),t._v(" "),s("span",{staticClass:"json-tree-indent"},[t._v(" ")])],2)])])])},staticRenderFns:[]};e.a=a},HZ3N:function(t,e){},"KK2+":function(t,e,s){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};e.default={name:"json-tree",props:{level:{type:Number,default:1/0},kv:{type:Object},raw:{type:String},data:{}},data:function(){return{expanded:!0,hovered:!1}},computed:{parsed:function(){if(this.kv)return this.kv;var t=void 0;try{this.raw?t=JSON.parse(this.raw):void 0!==this.data?t=this.data:(t="[Vue JSON Tree] No data passed.",console.warn(t))}catch(e){t="[Vue JSON Tree] Invalid raw JSON.",console.warn(t)}finally{return function t(e){var s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=!(arguments.length>2&&void 0!==arguments[2])||arguments[2],i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:void 0,r={depth:s,last:n,primitive:!0,key:JSON.stringify(i)};if("object"!==(void 0===e?"undefined":a(e)))return Object.assign(r,{type:void 0===e?"undefined":a(e),value:JSON.stringify(e)});if(null===e)return Object.assign(r,{type:"null",value:"null"});if(Array.isArray(e)){var o=e.map(function(a,n){return t(a,s+1,n===e.length-1)});return Object.assign(r,{primitive:!1,type:"array",value:o})}var c=Object.keys(e),l=c.map(function(a,n){return t(e[a],s+1,n===c.length-1,a)});return Object.assign(r,{primitive:!1,type:"object",value:l})}(t)}}},methods:{format:function(t){return t>1?t+" items":t?"1 item":"no items"}},created:function(){this.expanded=this.parsed.depth1e3&&t.messages.pop())},100)),this.timerActive=!0,setTimeout(function t(){var s=e.latest.messages.shift();s&&e.timerActive&&(e.messages.unshift(s),setTimeout(t,1e3*Math.random()+500))},2500)},toggleActiveMessage:function(t){this.activeMessage&&(this.activeMessage.active=!1),t.active=!0,this.activeMessage=t},toggleFullscreen:function(){this.fullscreenMessageViewer=!this.fullscreenMessageViewer,this.fullscreenMessageViewer&&this.$scrollTo(".message-holder",500),document.documentElement.classList.toggle("scroll-disabled")}},computed:{activeMessageContent:function(){return this.activeMessage?c()(this.activeMessage,null,2):"Hover over a message on the left!"}}},f={render:function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("section",{staticClass:"section demo-panel",attrs:{id:"demo"}},[t._m(0),t._v(" "),s("div",{staticClass:"columns connect-button"},[s("div",{staticClass:"column"},[t.state===t.states.STATE_DISCONNECTED?[s("a",{staticClass:"button",on:{click:t.connectWebsockets}},[t._v("\n OPEN THE FIRE HOSE\n ")])]:t.state===t.states.STATE_CONNECTING?[s("a",{staticClass:"button connecting"},[t._v("\n CONNECTING...\n ")])]:t.state===t.states.STATE_CONNECTED?[s("a",{staticClass:"button connected",on:{click:t.connectWebsockets}},[t._v("\n CONNECTED. CLICK TO DISCONNECT "),s("p",{staticClass:"heart-icon"},[s("i",{directives:[{name:"tooltip",rawName:"v-tooltip.top-center",value:{content:"Beats when a heartbeat is received"},expression:"{content: 'Beats when a heartbeat is received'}",modifiers:{"top-center":!0}}],staticClass:"fa fa-heart",attrs:{"aria-hidden":"true"}}),t._v("️")])])]:t.state===t.states.STATE_ERROR?[s("a",{staticClass:"button error",on:{click:t.connectWebsockets}},[t._v("\n ERROR CONNECTING! CLICK TO TRY AGAIN.\n ")])]:t._e()],2)]),t._v(" "),s("transition",{attrs:{name:"slide-toggle"}},[t.state!==t.states.STATE_DISCONNECTED?s("div",{staticClass:"columns message-holder",class:{fullscreen:t.fullscreenMessageViewer}},[0===t.messages.length?[s("div",{staticClass:"column holder-column"},[s("p",{staticClass:"empty-holder"},[t._v("\n Waiting on certificates..."),s("span",{staticClass:"wave"},[t._v("🌊")])])])]:[s("div",{staticClass:"full-screen-button",on:{click:t.toggleFullscreen}},[t.fullscreenMessageViewer?s("i",{staticClass:"fa fa-compress-alt",attrs:{"aria-hidden":"true"}}):s("i",{staticClass:"fa fa-expand-alt",attrs:{"aria-hidden":"true"}})]),t._v(" "),s("div",{staticClass:"column incoming-list"},[s("transition-group",{attrs:{name:"custom-classes-transition","enter-active-class":"animated fadeIn"}},t._l(t.messages,function(e){return s("div",{key:e.data.seen},[s("p",{staticClass:"line",class:{active:e.active},on:{mouseover:function(s){return t.toggleActiveMessage(e)}}},[t._v("\n ["+t._s(e.data.cert_index)+"] "+t._s(e.data.source.url)+" - "+t._s(e.data.leaf_cert.subject.CN)+"\n ")])])}),0)],1),t._v(" "),s("div",{staticClass:"column raw-content"},[s("div",{staticClass:"json-tree-wrapper"},[s("json-tree",{attrs:{data:t.activeMessage,level:4}})],1)])]],2):t._e()])],1)},staticRenderFns:[function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"columns call-to-action"},[e("div",{staticClass:"column"},[e("p",[this._v("TRY IT!")])])])}]};var E=s("VU/8")(h,f,!1,function(t){s("WU9O")},null,null).exports,B=s("njrj"),b=s.n(B),I={message_type:"heartbeat",timestamp:(new Date).getTime()/1e3},w=s("b6LA"),Q={name:"frontpage",data:function(){return{activeLanguage:null,activeDemo:{},typer:null,languages:{python:{install:"pip install certstream"},javascript:{install:"npm install certstream"},go:{install:"go get github.com/CaliDog/certstream-go"},java:{install:"Click Here for instructions (because Java 😓 ️)"}},demos:{basic:{name:"basic",command:"certstream",video:"https://storage.googleapis.com/certstream-artifacts/certstream.mp4"},full:{name:"full",command:"certstream --full",video:"https://storage.googleapis.com/certstream-artifacts/certstream-full.mp4"},json:{name:"json",command:"certstream --json | jq -r '.data.leaf_cert.all_domains[]'",video:"https://storage.googleapis.com/certstream-artifacts/certstream-json.mp4"}},heartbeat:I,exampleMessage:w}},mounted:function(){this.typer=new b.a(".typer",{strings:["Select a language above"],showCursor:!1}),this.demoTyper=new b.a(".demo-typer",{strings:["certstream"],showCursor:!1}),this.activeDemo=this.demos.basic},methods:{scrollDown:function(){this.$scrollTo("#intro-panel",500)},setActiveDemo:function(t){this.demos[t]!==this.activeDemo&&(this.activeDemo=this.demos[t],this.demoTyper.strings=[this.activeDemo.command],this.demoTyper.reset())},showPipInstructions:function(){this.$scrollTo("#install",500),this.setLanguage("python")},showToolTip:function(){this.$refs.clipboard._tooltip.show(),this.copyToClipboard(this.languages[this.activeLanguage].install)},hideToolTip:function(){setTimeout(this.$refs.clipboard._tooltip.hide,1e3)},setLanguage:function(t){this.activeLanguage!==t&&(this.activeLanguage=t,this.typer.strings=[this.languages[t].install],this.typer.reset())},copyToClipboard:function(t){if(window.clipboardData&&window.clipboardData.setData)return window.clipboardData.setData("Text",t);if(document.queryCommandSupported&&document.queryCommandSupported("copy")){var e=document.createElement("textarea");e.textContent=t,e.style.position="fixed",document.body.appendChild(e),e.select();try{return document.execCommand("copy")}catch(t){return console.warn("Copy to clipboard failed.",t),!1}finally{document.body.removeChild(e)}}}},components:{FeedWatcher:E,JsonTree:v.default},computed:{currentYear:function(){return(new Date).getFullYear()}}},y={render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{staticClass:"main-wrapper"},[a("section",{staticClass:"section top-panel"},[a("div",{staticClass:"container"},[a("div",{staticClass:"columns"},[a("div",{staticClass:"column"}),t._v(" "),a("div",{staticClass:"column splash"},[a("h1",{staticClass:"title animated fadeInDown"},[t._v("\n CERTSTREAM\n ")]),t._v(" "),t._m(0),t._v(" "),a("a",{staticClass:"button learn-more animated fadeIn slow delayed",on:{click:t.scrollDown}},[t._v("Learn More")])])])])]),t._v(" "),a("img",{staticClass:"transition",attrs:{id:"rolling-transition",src:s("Mx5b"),alt:"rolling-transition"}}),t._v(" "),t._m(1),t._v(" "),a("feed-watcher"),t._v(" "),a("section",{staticClass:"section get-started-panel"},[a("div",{staticClass:"container has-text-centered get-started-content"},[a("p",{staticClass:"title"},[t._v("GET STARTED")]),t._v(" "),a("div",{staticClass:"container has-text-centered"},[a("div",{staticClass:"columns"},[a("div",{staticClass:"column"},[a("div",{staticClass:"content-section"},[a("h2",{staticClass:"small-title",attrs:{id:"install"}},[t._v("Install CertStream")]),t._v(" "),t._m(2),t._v(" "),a("div",{staticClass:"columns language-buttons"},[a("div",{staticClass:"python column",class:{active:"python"===t.activeLanguage},on:{mouseover:function(e){return t.setLanguage("python")}}},[a("i",{staticClass:"devicon-python-plain",class:{colored:"python"===t.activeLanguage}}),t._v(" "),a("a",{attrs:{target:"_blank",href:"https://github.com/CaliDog/certstream-python"}},[t._v("Python")])]),t._v(" "),a("div",{staticClass:"javascript column",class:{active:"javascript"===t.activeLanguage},on:{mouseover:function(e){return t.setLanguage("javascript")}}},[a("i",{staticClass:"devicon-javascript-plain",class:{colored:"javascript"===t.activeLanguage}}),t._v(" "),a("a",{attrs:{target:"_blank",href:"https://github.com/CaliDog/certstream-js"}},[t._v("JavaScript")])]),t._v(" "),a("div",{staticClass:"go column",class:{active:"go"===t.activeLanguage},on:{mouseover:function(e){return t.setLanguage("go")}}},[a("i",{staticClass:"devicon-go-plain",class:{colored:"go"===t.activeLanguage}}),t._v(" "),a("a",{attrs:{target:"_blank",href:"https://github.com/CaliDog/certstream-go"}},[t._v("Go")])]),t._v(" "),a("div",{staticClass:"java column",class:{active:"java"===t.activeLanguage},on:{mouseover:function(e){return t.setLanguage("java")}}},[a("i",{staticClass:"devicon-java-plain",class:{colored:"java"===t.activeLanguage}}),t._v(" "),a("a",{attrs:{target:"_blank",href:"https://github.com/CaliDog/certstream-java"}},[t._v("Java")])])]),t._v(" "),a("div",{staticClass:"typer-wrapper"},[a("p",{staticClass:"content typer-content",class:{active:null!==t.activeLanguage&&"java"!==t.activeLanguage}},[a("span",{staticClass:"dollar"},[t._v("$")]),t._v(" "),a("span",{staticClass:"typer"}),t._v(" "),a("span",{directives:[{name:"tooltip",rawName:"v-tooltip.top-center",value:{content:"Copied to your clipboard!",trigger:"manual",hide:1e3},expression:"{content: 'Copied to your clipboard!', trigger: 'manual', hide: 1000}",modifiers:{"top-center":!0}}],ref:"clipboard",staticClass:"copy",on:{click:t.showToolTip,mouseleave:t.hideToolTip}},[a("i",{staticClass:"fa fa-clipboard",attrs:{"aria-hidden":"true"}})])])])]),t._v(" "),a("div",{staticClass:"content-section cli-example"},[a("h2",{staticClass:"small-title",attrs:{id:"cli"}},[t._v("CertStream CLI")]),t._v(" "),a("p",{staticClass:"white-text"},[t._v("\n Installing the CLI is easy, all you have to do is "),a("a",{on:{click:t.showPipInstructions}},[t._v("install the python library")]),t._v(" and run it like any other program. It can be used to emit certificate data in a number of forms to be processed by other command line utilities (or just for storage). Pipe it into grep, sed, awk, jq, or any other utility to send alerts, gather statistics, or slice and dice certs as you want!\n ")]),t._v(" "),a("div",{staticClass:"columns demo-gifs"},[a("div",{staticClass:"column"},[a("div",{staticClass:"columns demo-selector-wrapper"},[a("div",{staticClass:"column",class:{active:"basic"===t.activeDemo.name},on:{mouseover:function(e){return t.setActiveDemo("basic")}}},[t._m(3)]),t._v(" "),a("div",{staticClass:"column",class:{active:"full"===t.activeDemo.name},on:{mouseover:function(e){return t.setActiveDemo("full")}}},[t._m(4)]),t._v(" "),a("div",{staticClass:"column",class:{active:"json"===t.activeDemo.name},on:{mouseover:function(e){return t.setActiveDemo("json")}}},[t._m(5)])]),t._v(" "),a("div",{staticClass:"demo-data-wrapper"},[t._m(6),t._v(" "),a("div",{staticClass:"section-wrapper"},[a("video",{staticClass:"demo-video",attrs:{autoplay:"",muted:"",loop:"",controls:"",src:t.activeDemo.video},domProps:{muted:!0}})])])])])])])])])])]),t._v(" "),a("section",{staticClass:"section data-structures"},[a("div",{staticClass:"container has-text-centered data-structures-content"},[a("p",{staticClass:"title"},[t._v("SIMPLE(ISH) DATA")]),t._v(" "),a("div",{staticClass:"container has-text-centered"},[a("div",{staticClass:"columns"},[a("div",{staticClass:"column subsection-wrapper heartbeat-subsection"},[a("h2",{staticClass:"small-title"},[t._v("Heartbeat Messsages")]),t._v(" "),a("div",{staticClass:"json-tree-wrapper"},[a("json-tree",{staticClass:"tree-display",attrs:{data:t.heartbeat,level:4}})],1)])]),t._v(" "),a("div",{staticClass:"columns"},[a("div",{staticClass:"column subsection-wrapper update-subsection"},[a("h2",{staticClass:"small-title"},[t._v("Certificate Update")]),t._v(" "),t._m(7),t._v(" "),a("div",{staticClass:"json-tree-wrapper"},[a("json-tree",{staticClass:"tree-display",attrs:{data:t.exampleMessage,level:4}})],1)])])])])]),t._v(" "),a("section",{staticClass:"section footer"},[a("div",{staticClass:"container has-text-centered"},[a("div",{staticClass:"container has-text-centered"},[t._m(8),t._v(" "),t._m(9),t._v(" "),a("p",[t._v("© "+t._s(t.currentYear)+" Made with love by "),a("a",{staticClass:"footer-link",attrs:{href:"https://calidog.io/",target:"_blank"}},[t._v("Cali Dog Security")])])])])])],1)},staticRenderFns:[function(){var t=this.$createElement,e=this._self._c||t;return e("h2",{staticClass:"subtitle animated fadeIn slow delayed"},[this._v("\n Real-time "),e("a",{attrs:{href:"https://www.certificate-transparency.org/what-is-ct"}},[this._v("certificate transparency log")]),this._v(" update stream.\n "),e("br"),this._v("\n See SSL certificates as they're issued in real time.\n ")])},function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("section",{staticClass:"section intro-panel",attrs:{id:"intro-panel"}},[a("div",{staticClass:"container has-text-centered"},[a("div",{staticClass:"columns"},[a("div",{staticClass:"column has-text-centered"},[a("img",{staticClass:"overview",attrs:{src:s("Ovzb")}})]),t._v(" "),a("div",{staticClass:"column right-column"},[a("p",{staticClass:"title"},[t._v("TL;DR")]),t._v(" "),a("p",{staticClass:"content"},[t._v("\n CertStream is an intelligence feed that gives you real-time updates from the "),a("a",{attrs:{href:"https://www.certificate-transparency.org/what-is-ct"}},[t._v("Certificate\n Transparency Log network")]),t._v(", allowing you to use it as a building block to make tools that react to new certificates being\n issued in real time. We do all the hard work of watching, aggregating, and parsing the transparency logs, and give you super simple\n libraries that enable you to do awesome things with minimal effort.\n "),a("br"),a("br"),t._v('\n It\'s our way of saying "thank you" to the amazing security community in general, as well as a\n good way to give people a taste of the sort of intelligence feeds that are powering our flagship\n product - '),a("a",{attrs:{href:"https://phishfinder.io",target:"_blank"}},[t._v("PhishFinder")]),t._v(".\n ")])])])])])},function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("p",{staticClass:"white-text"},[t._v("\n CertStream is hosted "),s("a",{attrs:{href:"https://github.com/search?q=org%3ACaliDog+certstream"}},[t._v("on Github")]),t._v(" and we currently have libraries for "),s("a",{attrs:{href:"https://github.com/CaliDog/certstream-python"}},[t._v("Python")]),t._v(", "),s("a",{attrs:{href:"https://github.com/CaliDog/certstream-js"}},[t._v("Javascript")]),t._v(", "),s("a",{attrs:{href:"https://github.com/CaliDog/certstream-go"}},[t._v("Go")]),t._v(", and "),s("a",{attrs:{href:"https://github.com/CaliDog/certstream-java"}},[t._v("Java")]),t._v(".\n These libraries are intended to lower the barrier of entry to interacting with the "),s("a",{attrs:{href:"https://www.certificate-transparency.org/what-is-ct"}},[t._v("Certificate Transparency Log")]),t._v(" network so you can craft simple but powerful analytics tools with just a few lines of code!\n ")])},function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"demo-selector"},[e("p",[this._v("Basic output")])])},function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"demo-selector"},[e("p",[this._v("Full SAN output")])])},function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"demo-selector"},[e("p",[this._v("JSON output mode + JQ")])])},function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"command-wrapper"},[e("p",{staticClass:"content typer-content"},[e("span",{staticClass:"dollar"},[this._v("$")]),this._v(" "),e("span",{staticClass:"demo-typer"})])])},function(){var t=this.$createElement,e=this._self._c||t;return e("p",[this._v("If you prefer the raw data blob, there's a live example "),e("a",{attrs:{target:"_blank",href:"/example.json"}},[this._v("here")])])},function(){var t=this.$createElement,e=this._self._c||t;return e("a",{attrs:{href:"https://calidog.io/",target:"_blank"}},[e("img",{staticClass:"doghead",attrs:{src:s("oncf")}})])},function(){var t=this.$createElement,e=this._self._c||t;return e("span",{staticClass:"icons"},[e("a",{attrs:{target:"_blank",href:"https://medium.com/cali-dog-security"}},[e("i",{staticClass:"fab fa-medium-m",attrs:{"aria-hidden":"true"}})]),this._v(" "),e("a",{attrs:{target:"_blank",href:"https://github.com/calidog"}},[e("i",{staticClass:"fab fa-github",attrs:{"aria-hidden":"true"}})])])}]};var _=s("VU/8")(Q,y,!1,function(t){s("HZ3N")},null,null).exports;a.a.use(r.a);var T=new r.a({mode:"history",routes:[{path:"/",name:"Frontpage",component:_}]}),D=s("VN6J"),N=s("bm7V"),j=s.n(N);a.a.use(D.a),a.a.use(j.a),a.a.config.productionTip=!1,window.vueInstance=new a.a({el:"#app",router:T,render:function(t){return t(i)}})},Ovzb:function(t,e,s){t.exports=s.p+"static/img/certstream-overview.png"},VBZT:function(t,e){},WU9O:function(t,e){},b6LA:function(t,e){t.exports={data:{cert_index:343999487,cert_link:"http://ct.googleapis.com/logs/argon2020/ct/v1/get-entries?start=343999487&end=343999487",chain:[{extensions:{authorityInfoAccess:"CA Issuers - URI:http://apps.identrust.com/roots/dstrootcax3.p7c\nOCSP - URI:http://isrg.trustid.ocsp.identrust.com\n",authorityKeyIdentifier:"keyid:C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10\n",basicConstraints:"CA:TRUE",certificatePolicies:"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.root-x1.letsencrypt.org",crlDistributionPoints:"Full Name:\n URI:http://crl.identrust.com/DSTROOTCAX3CRL.crl",keyUsage:"Digital Signature, Key Cert Sign, C R L Sign",subjectKeyIdentifier:"A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1"},fingerprint:"E6:A3:B4:5B:06:2D:50:9B:33:82:28:2D:19:6E:FE:97:D5:95:6C:CB",not_after:1615999246,not_before:1458232846,serial_number:"A0141420000015385736A0B85ECA708",subject:{C:"US",CN:"Let's Encrypt Authority X3",L:null,O:"Let's Encrypt",OU:null,ST:null,aggregated:"/C=US/CN=Let's Encrypt Authority X3/O=Let's Encrypt"}},{extensions:{basicConstraints:"CA:TRUE",keyUsage:"Key Cert Sign, C R L Sign",subjectKeyIdentifier:"C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10"},fingerprint:"DA:C9:02:4F:54:D8:F6:DF:94:93:5F:B1:73:26:38:CA:6A:D7:7C:13",not_after:1633010475,not_before:970348339,serial_number:"44AFB080D6A327BA893039862EF8406B",subject:{C:null,CN:"DST Root CA X3",L:null,O:"Digital Signature Trust Co.",OU:null,ST:null,aggregated:"/CN=DST Root CA X3/O=Digital Signature Trust Co."}}],leaf_cert:{all_domains:["access.smm-cheap.com","www.access.smm-cheap.com"],extensions:{authorityInfoAccess:"CA Issuers - URI:http://cert.int-x3.letsencrypt.org/\nOCSP - URI:http://ocsp.int-x3.letsencrypt.org\n",authorityKeyIdentifier:"keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1\n",basicConstraints:"CA:FALSE",certificatePolicies:"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.letsencrypt.org",ctlSignedCertificateTimestamp:"BIHxAO8AdgBep3P531bA57U2SH3QSeAyepGaDIShEhKEGHWWgXFFWAAAAXBHIXo_AAAEAwBHMEUCIAJzKm8r6y8DH86jiH3p3nvee06E51GMyFXk_tCBou_kAiEAtFTGmtfDCElSeSpZscF_WDLeIp2NjQvoyl_jQHR3u6cAdQCyHgXMi6LNiiBOh2b5K7mKJSBna9r6cOeySVMt74uQXgAAAXBHIXotAAAEAwBGMEQCIDZ9wja7HJtLqHppVV6QHdfzeBM1PBEid9bmz3x2eyGwAiByBwCZux2N8qpF4N5cPYm5CV6gQBSb4aMmO1v0UQHaQw==",extendedKeyUsage:"TLS Web server authentication, TLS Web client authentication",keyUsage:"Digital Signature, Key Encipherment",subjectAltName:"DNS:www.access.smm-cheap.com, DNS:access.smm-cheap.com",subjectKeyIdentifier:"71:23:2B:93:50:A0:60:11:EF:7A:29:9D:23:99:80:AF:06:68:40:FF"},fingerprint:"84:E0:18:01:52:93:08:E0:BC:1C:DA:C8:B4:48:64:76:8A:61:56:32",not_after:1589513741,not_before:1581737741,serial_number:"37A3A72033B9AFF73A4BED5BB4F7CE6746E",subject:{C:null,CN:"access.smm-cheap.com",L:null,O:null,OU:null,ST:null,aggregated:"/CN=access.smm-cheap.com"}},seen:1581741558.477939,source:{name:"Google 'Argon2020' log",url:"ct.googleapis.com/logs/argon2020/"},update_type:"X509LogEntry"},message_type:"certificate_update"}},h1pU:function(t,e,s){"use strict";var a=s("KK2+"),n=s.n(a),i=s("2+D3");var r=function(t){s("VBZT")},o=s("VU/8")(n.a,i.a,!1,r,null,null);e.default=o.exports},oncf:function(t,e,s){t.exports=s.p+"static/img/doghead.png"}},["NHnr"]); 2 | //# sourceMappingURL=app.47135fa.js.map -------------------------------------------------------------------------------- /frontend/dist/static/js/manifest.47135fa.js: -------------------------------------------------------------------------------- 1 | !function(r){var n=window.webpackJsonp;window.webpackJsonp=function(e,u,c){for(var f,i,p,a=0,l=[];a 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Certstream 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "certstream", 3 | "version": "1.0.0", 4 | "description": "", 5 | "author": "Ryan Sears ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack --watch --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "lint": "eslint --ext .js,.vue src", 11 | "fix": "eslint --fix --ext .js,.vue src", 12 | "build": "node build/build.js" 13 | }, 14 | "dependencies": { 15 | "@fortawesome/fontawesome-free": "^5.12.1", 16 | "animate.css": "^3.7.2", 17 | "animejs": "^3.1.0", 18 | "axios": "^0.21.1", 19 | "bulma": "^0.8.0", 20 | "debounce": "^1.2.0", 21 | "robust-websocket": "^1.0.0", 22 | "typed.js": "^2.0.11", 23 | "v-tooltip": "^2.0.3", 24 | "vue": "^2.5.2", 25 | "vue-json-tree": "0.3.3", 26 | "vue-router": "^3.0.1", 27 | "vue-scrollto": "^2.17.1" 28 | }, 29 | "devDependencies": { 30 | "autoprefixer": "^7.1.2", 31 | "babel-core": "^6.22.1", 32 | "babel-eslint": "^8.2.1", 33 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 34 | "babel-loader": "^7.1.1", 35 | "babel-plugin-syntax-jsx": "^6.18.0", 36 | "babel-plugin-transform-runtime": "^6.22.0", 37 | "babel-plugin-transform-vue-jsx": "^3.5.0", 38 | "babel-preset-env": "^1.7.0", 39 | "babel-preset-es2015": "^6.24.1", 40 | "babel-preset-stage-2": "^6.22.0", 41 | "chalk": "^2.0.1", 42 | "copy-webpack-plugin": "^4.0.1", 43 | "css-loader": "^0.28.0", 44 | "eslint": "^4.15.0", 45 | "eslint-config-standard": "^10.2.1", 46 | "eslint-friendly-formatter": "^3.0.0", 47 | "eslint-loader": "^1.7.1", 48 | "eslint-plugin-import": "^2.7.0", 49 | "eslint-plugin-node": "^5.2.0", 50 | "eslint-plugin-promise": "^3.4.0", 51 | "eslint-plugin-standard": "^3.0.1", 52 | "eslint-plugin-vue": "^4.0.0", 53 | "extract-text-webpack-plugin": "^3.0.0", 54 | "file-loader": "^1.1.4", 55 | "friendly-errors-webpack-plugin": "^1.6.1", 56 | "html-webpack-plugin": "^2.30.1", 57 | "node-notifier": "^8.0.1", 58 | "node-sass": "^4.13.1", 59 | "optimize-css-assets-webpack-plugin": "^3.2.0", 60 | "ora": "^1.2.0", 61 | "portfinder": "^1.0.13", 62 | "postcss-import": "^11.0.0", 63 | "postcss-loader": "^2.0.8", 64 | "postcss-url": "^7.2.1", 65 | "rimraf": "^2.6.0", 66 | "sass-loader": "^7.3.1", 67 | "semver": "^5.3.0", 68 | "shelljs": "^0.7.6", 69 | "uglifyjs-webpack-plugin": "^1.1.1", 70 | "url-loader": "^0.5.8", 71 | "vue-loader": "^13.7.3", 72 | "vue-style-loader": "^3.0.1", 73 | "vue-template-compiler": "^2.5.2", 74 | "webpack": "^3.6.0", 75 | "webpack-bundle-analyzer": "^3.3.2", 76 | "webpack-dev-server": "^3.11.0", 77 | "webpack-merge": "^4.1.0" 78 | }, 79 | "engines": { 80 | "node": ">= 6.0.0", 81 | "npm": ">= 3.0.0" 82 | }, 83 | "browserslist": [ 84 | "> 1%", 85 | "last 2 versions", 86 | "not ie <= 8" 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 76 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend/src/assets/devicon-colors.css: -------------------------------------------------------------------------------- 1 | .devicon-sketch-line-wordmark.colored, 2 | .devicon-sketch-line.colored { 3 | color: #fdad00; 4 | } 5 | .devicon-npm-original-wordmark.colored { 6 | color: #cb3837; 7 | } 8 | .devicon-ionic-original-wordmark.colored, 9 | .devicon-ionic-original.colored { 10 | color: #4e8ef7; 11 | } 12 | .devicon-ember-original-wordmark.colored { 13 | color: #dd3f24; 14 | } 15 | .devicon-electron-original-wordmark.colored, 16 | .devicon-electron-original.colored { 17 | color: #47848f; 18 | } 19 | .devicon-vagrant-plain-wordmark.colored, 20 | .devicon-vagrant-plain.colored { 21 | color: #127eff; 22 | } 23 | .devicon-yarn-plain-wordmark.colored, 24 | .devicon-yarn-plain.colored { 25 | color: #2c8ebb; 26 | } 27 | .devicon-handlebars-plain-wordmark.colored, 28 | .devicon-handlebars-plain.colored{ 29 | color: #000000; 30 | } 31 | .devicon-couchdb-plain-wordmark.colored, 32 | .devicon-couchdb-plain.colored { 33 | color: #e42528; 34 | } 35 | .devicon-behance-plain-wordmark.colored, 36 | .devicon-behance-plain.colored { 37 | color: #0071e0; 38 | } 39 | .devicon-linkedin-plain-wordmark.colored, 40 | .devicon-linkedin-plain.colored { 41 | color: #0076b2; 42 | } 43 | .devicon-ceylon-plain-wordmark.colored, 44 | .devicon-ceylon-plain.colored { 45 | color: #AB710A; 46 | } 47 | .devicon-elm-plain-wordmark.colored, 48 | .devicon-elm-plain.colored { 49 | color: #34495E; 50 | } 51 | .devicon-cakephp-plain-wordmark.colored, 52 | .devicon-cakephp-plain.colored { 53 | color: #D43D44; 54 | } 55 | .devicon-stylus-original.colored { 56 | color: #333333; 57 | } 58 | .devicon-express-original-wordmark.colored, 59 | .devicon-express-original.colored { 60 | color: #444; 61 | } 62 | .devicon-devicon-plain-wordmark.colored, 63 | .devicon-devicon-plain.colored { 64 | color: #60BE86; 65 | } 66 | .devicon-intellij-plain-wordmark.colored, 67 | .devicon-intellij-plain.colored { 68 | color: #136BA2; 69 | } 70 | .devicon-pycharm-plain-wordmark.colored, 71 | .devicon-pycharm-plain.colored { 72 | color: #4D8548; 73 | } 74 | .devicon-rubymine-plain-wordmark.colored, 75 | .devicon-rubymine-plain.colored { 76 | color: #C12C4C; 77 | } 78 | .devicon-webstorm-plain-wordmark.colored, 79 | .devicon-webstorm-plain.colored { 80 | color: #2788B5; 81 | } 82 | .devicon-tomcat-line-wordmark.colored, 83 | .devicon-tomcat-line.colored { 84 | color: #D1A41A; 85 | } 86 | .devicon-vuejs-line-wordmark.colored, 87 | .devicon-vuejs-line.colored, 88 | .devicon-vuejs-plain-wordmark.colored, 89 | .devicon-vuejs-plain.colored { 90 | color: #41B883; 91 | } 92 | .devicon-swift-plain-wordmark.colored, 93 | .devicon-swift-plain.colored { 94 | color: #F05138; 95 | } 96 | .devicon-webpack-plain-wordmark.colored, 97 | .devicon-webpack-plain.colored { 98 | color: #1C78C0; 99 | } 100 | .devicon-visualstudio-plain-wordmark.colored, 101 | .devicon-visualstudio-plain.colored { 102 | color: #68217A; 103 | } 104 | .devicon-slack-plain-wordmark.colored, 105 | .devicon-slack-plain.colored { 106 | color: #2D333A; 107 | } 108 | .devicon-gatling-plain-wordmark.colored { 109 | color: #E77500; 110 | } 111 | .devicon-gatling-plain.colored { 112 | color: #E77500; 113 | } 114 | .devicon-ssh-original-wordmark.colored, 115 | .devicon-ssh-plain-wordmark.colored { 116 | color: #231F20; 117 | } 118 | .devicon-ssh-original.colored, 119 | .devicon-ssh-plain.colored { 120 | color: #231F20; 121 | } 122 | .devicon-sourcetree-original-wordmark.colored, 123 | .devicon-sourcetree-plain-wordmark.colored { 124 | color: #205081; 125 | } 126 | .devicon-sourcetree-original.colored, 127 | .devicon-sourcetree-plain.colored { 128 | color: #205081; 129 | } 130 | .devicon-phpstorm-plain-wordmark.colored { 131 | color: #5058A6; 132 | } 133 | .devicon-phpstorm-plain.colored { 134 | color: #5058A6; 135 | } 136 | .devicon-protractor-plain-wordmark.colored { 137 | color: #b7111d; 138 | } 139 | .devicon-protractor-plain.colored { 140 | color: #b7111d; 141 | } 142 | .devicon-cucumber-plain-wordmark.colored { 143 | color: #00a818; 144 | } 145 | .devicon-cucumber-plain.colored { 146 | color: #00a818; 147 | } 148 | .devicon-gradle-plain-wordmark.colored { 149 | color: #02303a; 150 | } 151 | .devicon-gradle-plain.colored { 152 | color: #02303a; 153 | } 154 | .devicon-jeet-plain-wordmark.colored { 155 | color: #FF664A; 156 | } 157 | .devicon-jeet-plain.colored { 158 | color: #FF664A; 159 | } 160 | .devicon-gitlab-plain-wordmark.colored { 161 | color: #E24329; 162 | } 163 | .devicon-gitlab-plain.colored { 164 | color: #E24329; 165 | } 166 | .devicon-github-original-wordmark.colored, 167 | .devicon-github-plain-wordmark.colored { 168 | color: #181616; 169 | } 170 | .devicon-github-original.colored, 171 | .devicon-github-plain.colored { 172 | color: #181616; 173 | } 174 | .devicon-d3js-plain.colored { 175 | color: #f7974e; 176 | } 177 | .devicon-confluence-original-wordmark.colored, 178 | .devicon-confluence-plain-wordmark.colored { 179 | color: #205081; 180 | } 181 | .devicon-confluence-original.colored, 182 | .devicon-confluence-plain.colored { 183 | color: #205081; 184 | } 185 | .devicon-bitbucket-original-wordmark.colored, 186 | .devicon-bitbucket-plain-wordmark.colored { 187 | color: #205081; 188 | } 189 | .devicon-bitbucket-original.colored, 190 | .devicon-bitbucket-plain.colored { 191 | color: #205081; 192 | } 193 | .devicon-amazonwebservices-plain-wordmark.colored, 194 | .devicon-amazonwebservices-original.colored, 195 | .devicon-amazonwebservices-plain.colored { 196 | color: #F7A80D; 197 | } 198 | .devicon-android-plain-wordmark.colored, 199 | .devicon-android-plain.colored { 200 | color: #A4C439; 201 | } 202 | .devicon-angularjs-plain-wordmark.colored, 203 | .devicon-angularjs-plain.colored { 204 | color: #c4473a; 205 | } 206 | .devicon-apache-line-wordmark.colored, 207 | .devicon-apache-line.colored, 208 | .devicon-apache-plain-wordmark.colored, 209 | .devicon-apache-plain.colored { 210 | color: #303284; 211 | } 212 | .devicon-appcelerator-plain-wordmark.colored, 213 | .devicon-appcelerator-original.colored, 214 | .devicon-appcelerator-plain.colored { 215 | color: #ac162c; 216 | } 217 | .devicon-apple-original.colored, 218 | .devicon-apple-plain.colored { 219 | color: #000000; 220 | } 221 | .devicon-atom-original.colored, 222 | .devicon-atom-original-wordmark.colored, 223 | .devicon-atom-plain.colored, 224 | .devicon-atom-plain-wordmark.colored { 225 | color: #67595D; 226 | } 227 | .devicon-babel-original.colored, 228 | .devicon-babel-plain.colored { 229 | color: #f9dc3e; 230 | } 231 | .devicon-backbonejs-plain.colored, 232 | .devicon-backbonejs-plain-wordmark.colored { 233 | color: #002A41; 234 | } 235 | .devicon-bootstrap-plain-wordmark.colored, 236 | .devicon-bootstrap-plain.colored { 237 | color: #59407f; 238 | } 239 | .devicon-bower-line-wordmark.colored, 240 | .devicon-bower-line.colored, 241 | .devicon-bower-plain-wordmark.colored, 242 | .devicon-bower-plain.colored { 243 | color: #ef5734; 244 | } 245 | .devicon-c-line-wordmark.colored, 246 | .devicon-c-line.colored, 247 | .devicon-c-plain-wordmark.colored, 248 | .devicon-c-plain.colored { 249 | color: #03599c; 250 | } 251 | .devicon-c-line-wordmark.colored, 252 | .devicon-c-line.colored, 253 | .devicon-c-plain-wordmark.colored, 254 | .devicon-c-plain.colored { 255 | color: #03599c; 256 | } 257 | .devicon-chrome-plain-wordmark.colored, 258 | .devicon-chrome-plain.colored { 259 | color: #ce4e4e; 260 | } 261 | .devicon-codeigniter-plain-wordmark.colored, 262 | .devicon-codeigniter-plain.colored { 263 | color: #ee4323; 264 | } 265 | .devicon-coffeescript-original-wordmark.colored, 266 | .devicon-coffeescript-original.colored, 267 | .devicon-coffeescript-plain-wordmark.colored, 268 | .devicon-coffeescript-plain.colored { 269 | color: #28334c; 270 | } 271 | .devicon-cplusplus-line-wordmark.colored, 272 | .devicon-cplusplus-line.colored, 273 | .devicon-cplusplus-plain-wordmark.colored, 274 | .devicon-cplusplus-plain.colored { 275 | color: #9c033a; 276 | } 277 | .devicon-csharp-line-wordmark.colored, 278 | .devicon-csharp-line.colored, 279 | .devicon-csharp-plain-wordmark.colored, 280 | .devicon-csharp-plain.colored { 281 | color: #68217a; 282 | } 283 | .devicon-css3-plain-wordmark.colored, 284 | .devicon-css3-plain.colored { 285 | color: #3d8fc6; 286 | } 287 | .devicon-debian-plain-wordmark.colored, 288 | .devicon-debian-plain.colored { 289 | color: #A80030; 290 | } 291 | .devicon-django-line.colored, 292 | .devicon-django-line-wordmark.colored, 293 | .devicon-django-plain.colored, 294 | .devicon-django-plain-wordmark.colored { 295 | color: #003A2B; 296 | } 297 | .devicon-docker-plain-wordmark.colored, 298 | .devicon-docker-plain.colored { 299 | color: #019bc6; 300 | } 301 | .devicon-doctrine-line-wordmark.colored, 302 | .devicon-doctrine-line.colored, 303 | .devicon-doctrine-plain-wordmark.colored, 304 | .devicon-doctrine-plain.colored { 305 | color: #f56d39; 306 | } 307 | .devicon-dot-net-plain-wordmark.colored, 308 | .devicon-dot-net-plain.colored { 309 | color: #1384c8; 310 | } 311 | .devicon-drupal-plain-wordmark.colored, 312 | .devicon-drupal-plain.colored { 313 | color: #0073BA; 314 | } 315 | .devicon-erlang-plain-wordmark.colored, 316 | .devicon-erlang-plain.colored { 317 | color: #a90533; 318 | } 319 | .devicon-facebook-original.colored, 320 | .devicon-facebook-plain.colored { 321 | color: #3d5a98; 322 | } 323 | .devicon-firefox-plain-wordmark.colored, 324 | .devicon-firefox-plain.colored { 325 | color: #DD732A; 326 | } 327 | .devicon-foundation-plain-wordmark.colored, 328 | .devicon-foundation-plain.colored { 329 | color: #008cba; 330 | } 331 | .devicon-gimp-plain-wordmark.colored, 332 | .devicon-gimp-plain.colored { 333 | color: #716955; 334 | } 335 | .devicon-git-plain-wordmark.colored, 336 | .devicon-git-plain.colored { 337 | color: #f34f29; 338 | } 339 | .devicon-go-line.colored, 340 | .devicon-go-plain.colored { 341 | color: #000000; 342 | } 343 | .devicon-google-original-wordmark.colored, 344 | .devicon-google-plain-wordmark.colored { 345 | color: #587dbd; 346 | } 347 | .devicon-google-original.colored, 348 | .devicon-google-plain.colored { 349 | color: #587dbd; 350 | } 351 | .devicon-grunt-line-wordmark.colored, 352 | .devicon-grunt-line.colored, 353 | .devicon-grunt-plain-wordmark.colored, 354 | .devicon-grunt-plain.colored { 355 | color: #fcaa1a; 356 | } 357 | .devicon-gulp-plain.colored { 358 | color: #eb4a4b; 359 | } 360 | .devicon-heroku-line-wordmark.colored, 361 | .devicon-heroku-line.colored, 362 | .devicon-heroku-plain-wordmark.colored, 363 | .devicon-heroku-plain.colored, 364 | .devicon-heroku-original-wordmark.colored, 365 | .devicon-heroku-original.colored { 366 | color: #6762a6; 367 | } 368 | .devicon-html5-plain-wordmark.colored, 369 | .devicon-html5-plain.colored { 370 | color: #e54d26; 371 | } 372 | .devicon-ie10-original.colored, 373 | .devicon-ie10-plain.colored { 374 | color: #1EBBEE; 375 | } 376 | .devicon-illustrator-line.colored, 377 | .devicon-illustrator-plain.colored { 378 | color: #faa625; 379 | } 380 | .devicon-inkscape-plain-wordmark.colored, 381 | .devicon-inkscape-plain.colored { 382 | color: #000000; 383 | } 384 | .devicon-java-plain-wordmark.colored, 385 | .devicon-java-plain.colored { 386 | color: #EA2D2E; 387 | } 388 | .devicon-jasmine-plain-wordmark.colored, 389 | .devicon-jasmine-plain.colored { 390 | color: #8a4182; 391 | } 392 | .devicon-javascript-plain.colored { 393 | color: #f0db4f; 394 | } 395 | .devicon-jetbrains-plain.colored, 396 | .devicon-jetbrains-line.colored, 397 | .devicon-jetbrains-line-wordmark.colored, 398 | .devicon-jetbrains-plain-wordmark.colored { 399 | color: #F68B1F; 400 | } 401 | .devicon-jquery-plain-wordmark.colored, 402 | .devicon-jquery-plain.colored { 403 | color: #0769ad; 404 | } 405 | .devicon-krakenjs-plain.colored, 406 | .devicon-krakenjs-plain-wordmark.colored { 407 | color: #0081C2; 408 | } 409 | .devicon-laravel-plain-wordmark.colored, 410 | .devicon-laravel-plain.colored { 411 | color: #fd4f31; 412 | } 413 | .devicon-less-plain-wordmark.colored { 414 | color: #2a4d80; 415 | } 416 | .devicon-linux-plain.colored { 417 | color: #000000; 418 | } 419 | .devicon-meteor-plain.colored, 420 | .devicon-meteor-plain-wordmark.colored { 421 | color: #df5052; 422 | } 423 | .devicon-mocha-plain.colored { 424 | color: #8d6748; 425 | } 426 | .devicon-mongodb-plain.colored, 427 | .devicon-mongodb-plain-wordmark.colored { 428 | color: #4FAA41; 429 | } 430 | .devicon-moodle-plain.colored, 431 | .devicon-moodle-plain-wordmark.colored { 432 | color: #F7931E; 433 | } 434 | .devicon-mysql-plain.colored, 435 | .devicon-mysql-plain-wordmark.colored { 436 | color: #00618a; 437 | } 438 | .devicon-nginx-original.colored, 439 | .devicon-nginx-original-wordmark.colored, 440 | .devicon-nginx-plain.colored, 441 | .devicon-nginx-plain-wordmark.colored { 442 | color: #090; 443 | } 444 | .devicon-nodejs-plain.colored, 445 | .devicon-nodejs-plain-wordmark.colored { 446 | color: #83CD29; 447 | } 448 | .devicon-nodewebkit-line.colored, 449 | .devicon-nodewebkit-line-wordmark.colored, 450 | .devicon-nodewebkit-plain.colored, 451 | .devicon-nodewebkit-plain-wordmark.colored { 452 | color: #3d3b47; 453 | } 454 | .devicon-oracle-original.colored, 455 | .devicon-oracle-plain.colored, 456 | .devicon-oracle-plain-wordmark.colored { 457 | color: #EA1B22; 458 | } 459 | .devicon-photoshop-line.colored, 460 | .devicon-photoshop-plain.colored { 461 | color: #80b5e2; 462 | } 463 | .devicon-php-plain.colored { 464 | color: #6181b6; 465 | } 466 | .devicon-postgresql-plain.colored, 467 | .devicon-postgresql-plain-wordmark.colored { 468 | color: #336791; 469 | } 470 | .devicon-python-plain-wordmark.colored, 471 | .devicon-python-plain.colored { 472 | color: #ffd845; 473 | } 474 | .devicon-rails-plain-wordmark.colored, 475 | .devicon-rails-plain.colored { 476 | color: #a62c46; 477 | } 478 | .devicon-ruby-plain-wordmark.colored, 479 | .devicon-ruby-plain.colored { 480 | color: #d91404; 481 | } 482 | .devicon-safari-line-wordmark.colored, 483 | .devicon-safari-line.colored, 484 | .devicon-safari-plain-wordmark.colored, 485 | .devicon-safari-plain.colored { 486 | color: #1B88CA; 487 | } 488 | .devicon-react-plain-wordmark.colored, 489 | .devicon-react-plain.colored, 490 | .devicon-react-original-wordmark.colored, 491 | .devicon-react-original.colored { 492 | color: #61dafb; 493 | } 494 | .devicon-redhat-plain-wordmark.colored, 495 | .devicon-redhat-plain.colored, 496 | .devicon-redhat-original-wordmark.colored, 497 | .devicon-redhat-original.colored { 498 | color: #e93442; 499 | } 500 | .devicon-redis-plain-wordmark.colored, 501 | .devicon-redis-plain.colored{ 502 | color: #d82c20; 503 | } 504 | .devicon-ubuntu-plain-wordmark.colored, 505 | .devicon-ubuntu-plain.colored { 506 | color: #DD4814; 507 | } 508 | .devicon-sass-original.colored, 509 | .devicon-sass-plain.colored, 510 | .devicon-sass-plain-wordmark.colored { 511 | color: #cc6699; 512 | } 513 | .devicon-sequelize-original-wordmark.colored, 514 | .devicon-sequelize-plain-wordmark.colored, 515 | .devicon-sequelize-original.colored, 516 | .devicon-sequelize-plain.colored{ 517 | color: #3b4b72; 518 | } 519 | .devicon-symfony-original.colored, 520 | .devicon-symfony-original-wordmark.colored, 521 | .devicon-symfony-plain.colored, 522 | .devicon-symfony-plain-wordmark.colored { 523 | color: #1A171B; 524 | } 525 | .devicon-travis-plain-wordmark.colored, 526 | .devicon-travis-plain.colored { 527 | color: #bb2031; 528 | } 529 | .devicon-trello-plain-wordmark.colored, 530 | .devicon-trello-plain.colored { 531 | color: #23719f; 532 | } 533 | .devicon-twitter-original.colored, 534 | .devicon-twitter-plain.colored { 535 | color: #1da1f2; 536 | } 537 | .devicon-typescript-original.colored, 538 | .devicon-typescript-plain.colored { 539 | color: #007acc; 540 | } 541 | .devicon-ubuntu-plain-wordmark.colored, 542 | .devicon-ubuntu-plain.colored { 543 | color: #dd4814; 544 | } 545 | .devicon-vim-plain.colored { 546 | color: #179a33; 547 | } 548 | .devicon-windows8-original-wordmark.colored, 549 | .devicon-windows8-original.colored, 550 | .devicon-windows8-plain-wordmark.colored, 551 | .devicon-windows8-plain.colored { 552 | color: #00adef; 553 | } 554 | .devicon-wordpress-plain-wordmark.colored, 555 | .devicon-wordpress-plain.colored { 556 | color: #494949; 557 | } 558 | .devicon-yii-plain-wordmark.colored, 559 | .devicon-yii-plain.colored { 560 | color: #0073bb; 561 | } 562 | .devicon-zend-plain-wordmark.colored, 563 | .devicon-zend-plain.colored { 564 | color: #68b604; 565 | } -------------------------------------------------------------------------------- /frontend/src/assets/devicon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'devicon'; 3 | src:url('fonts/devicon.eot?-hdf3wh'); 4 | src:url('fonts/devicon.eot?#iefix-hdf3wh') format('embedded-opentype'), 5 | url('fonts/devicon.woff?-hdf3wh') format('woff'), 6 | url('fonts/devicon.ttf?-hdf3wh') format('truetype'), 7 | url('fonts/devicon.svg?-hdf3wh#devicon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="devicon-"], [class*=" devicon-"] { 13 | font-family: 'devicon'; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | .devicon-sketch-line-wordmark:before { 26 | content: "\e94c"; 27 | } 28 | .devicon-sketch-line:before { 29 | content: "\e94d"; 30 | } 31 | .devicon-npm-original-wordmark:before { 32 | content: "\e952"; 33 | } 34 | .devicon-ionic-original-wordmark:before { 35 | content: "\e953"; 36 | } 37 | .devicon-ionic-original:before { 38 | content: "\e954"; 39 | } 40 | .devicon-ember-original-wordmark:before { 41 | content: "\e955"; 42 | } 43 | .devicon-electron-original-wordmark:before { 44 | content: "\e956"; 45 | } 46 | .devicon-electron-original:before { 47 | content: "\e957"; 48 | } 49 | .devicon-vagrant-plain-wordmark:before { 50 | content: "\e94e"; 51 | } 52 | .devicon-vagrant-plain:before { 53 | content: "\e94f"; 54 | } 55 | .devicon-yarn-plain-wordmark:before { 56 | content: "\e950"; 57 | } 58 | .devicon-yarn-plain:before { 59 | content: "\e951"; 60 | } 61 | .devicon-handlebars-plain-wordmark:before { 62 | content: "\e94a"; 63 | } 64 | .devicon-handlebars-plain:before { 65 | content: "\e94b"; 66 | } 67 | .devicon-couchdb-plain-wordmark:before { 68 | content: "\e948"; 69 | } 70 | .devicon-couchdb-plain:before { 71 | content: "\e949"; 72 | } 73 | .devicon-behance-plain-wordmark:before { 74 | content: "\e943"; 75 | } 76 | .devicon-behance-plain:before { 77 | content: "\e945"; 78 | } 79 | .devicon-linkedin-plain-wordmark:before { 80 | content: "\e946"; 81 | } 82 | .devicon-linkedin-plain:before { 83 | content: "\e947"; 84 | } 85 | .devicon-ceylon-plain-wordmark:before { 86 | content: "\e943"; 87 | } 88 | .devicon-ceylon-plain:before { 89 | content: "\e944"; 90 | } 91 | .devicon-elm-plain-wordmark:before { 92 | content: "\e941"; 93 | } 94 | .devicon-elm-plain:before { 95 | content: "\e942"; 96 | } 97 | .devicon-cakephp-plain-wordmark:before { 98 | content: "\e93f"; 99 | } 100 | .devicon-cakephp-plain:before { 101 | content: "\e940"; 102 | } 103 | .devicon-stylus-original:before { 104 | content: "\e93e"; 105 | } 106 | .devicon-express-original-wordmark:before { 107 | content: "\e93c"; 108 | } 109 | .devicon-express-original:before { 110 | content: "\e93d"; 111 | } 112 | .devicon-devicon-plain-wordmark:before { 113 | content: "\e93a"; 114 | } 115 | .devicon-devicon-plain:before { 116 | content: "\e93b"; 117 | } 118 | .devicon-intellij-plain-wordmark:before { 119 | content: "\e932"; 120 | } 121 | .devicon-intellij-plain:before { 122 | content: "\e933"; 123 | } 124 | .devicon-pycharm-plain-wordmark:before { 125 | content: "\e934"; 126 | } 127 | .devicon-pycharm-plain:before { 128 | content: "\e935"; 129 | } 130 | .devicon-rubymine-plain-wordmark:before { 131 | content: "\e936"; 132 | } 133 | .devicon-rubymine-plain:before { 134 | content: "\e937"; 135 | } 136 | .devicon-webstorm-plain-wordmark:before { 137 | content: "\e938"; 138 | } 139 | .devicon-webstorm-plain:before { 140 | content: "\e939"; 141 | } 142 | .devicon-tomcat-line-wordmark:before { 143 | content: "\e92c"; 144 | } 145 | .devicon-tomcat-line:before { 146 | content: "\e92d"; 147 | } 148 | .devicon-vuejs-line-wordmark:before { 149 | content: "\e92e"; 150 | } 151 | .devicon-vuejs-line:before { 152 | content: "\e92f"; 153 | } 154 | .devicon-vuejs-plain-wordmark:before { 155 | content: "\e930"; 156 | } 157 | .devicon-vuejs-plain:before { 158 | content: "\e931"; 159 | } 160 | .devicon-swift-plain-wordmark:before { 161 | content: "\e92a"; 162 | } 163 | .devicon-swift-plain:before { 164 | content: "\e92b"; 165 | } 166 | .devicon-webpack-plain-wordmark:before { 167 | content: "\e928"; 168 | } 169 | .devicon-webpack-plain:before { 170 | content: "\e929"; 171 | } 172 | .devicon-visualstudio-plain-wordmark:before { 173 | content: "\e926"; 174 | } 175 | .devicon-visualstudio-plain:before { 176 | content: "\e927"; 177 | } 178 | .devicon-slack-plain-wordmark:before { 179 | content: "\e924"; 180 | } 181 | .devicon-slack-plain:before { 182 | content: "\e925"; 183 | } 184 | .devicon-facebook-original:before, 185 | .devicon-facebook-plain:before { 186 | content: "\e91c"; 187 | } 188 | .devicon-typescript-original:before, 189 | .devicon-typescript-plain:before { 190 | content: "\e920"; 191 | } 192 | .devicon-babel-original:before, 193 | .devicon-babel-plain:before { 194 | content: "\e921"; 195 | } 196 | .devicon-mocha-plain:before { 197 | content: "\e919"; 198 | } 199 | .devicon-jasmine-plain-wordmark:before { 200 | content: "\e91b"; 201 | } 202 | .devicon-jasmine-plain:before { 203 | content: "\e91a"; 204 | } 205 | .devicon-gatling-plain-wordmark:before { 206 | content: "\e918"; 207 | } 208 | .devicon-gatling-plain:before { 209 | content: "\e917"; 210 | } 211 | .devicon-ssh-original-wordmark:before, 212 | .devicon-ssh-plain-wordmark:before { 213 | content: "\e916"; 214 | } 215 | .devicon-ssh-original:before, 216 | .devicon-ssh-plain:before { 217 | content: "\e915"; 218 | } 219 | .devicon-sourcetree-original-wordmark:before, 220 | .devicon-sourcetree-plain-wordmark:before { 221 | content: "\e914"; 222 | } 223 | .devicon-sourcetree-original:before, 224 | .devicon-sourcetree-plain:before { 225 | content: "\e913"; 226 | } 227 | .devicon-phpstorm-plain-wordmark:before { 228 | content: "\e912"; 229 | } 230 | .devicon-phpstorm-plain:before { 231 | content: "\e911"; 232 | } 233 | .devicon-protractor-plain-wordmark:before { 234 | content: "\e901"; 235 | } 236 | .devicon-protractor-plain:before { 237 | content: "\e900"; 238 | } 239 | .devicon-gradle-plain-wordmark:before { 240 | content: "\e8f1"; 241 | } 242 | .devicon-gradle-plain:before { 243 | content: "\e902"; 244 | } 245 | .devicon-cucumber-plain-wordmark:before { 246 | content: "\e905"; 247 | } 248 | .devicon-cucumber-plain:before { 249 | content: "\e904"; 250 | } 251 | .devicon-jeet-plain-wordmark:before { 252 | content: "\e906"; 253 | } 254 | .devicon-jeet-plain:before { 255 | content: "\e903"; 256 | } 257 | .devicon-gitlab-plain-wordmark:before { 258 | content: "\e908"; 259 | } 260 | .devicon-gitlab-plain:before { 261 | content: "\e907"; 262 | } 263 | .devicon-github-original-wordmark:before, 264 | .devicon-github-plain-wordmark:before { 265 | content: "\e90a"; 266 | } 267 | .devicon-github-original:before, 268 | .devicon-github-plain:before { 269 | content: "\e909"; 270 | } 271 | .devicon-d3js-plain:before { 272 | content: "\e90c"; 273 | } 274 | .devicon-confluence-original-wordmark:before, 275 | .devicon-confluence-plain-wordmark:before { 276 | content: "\e90e"; 277 | } 278 | .devicon-confluence-original:before, 279 | .devicon-confluence-plain:before { 280 | content: "\e90d"; 281 | } 282 | .devicon-bitbucket-original-wordmark:before, 283 | .devicon-bitbucket-plain-wordmark:before { 284 | content: "\e910"; 285 | } 286 | .devicon-bitbucket-original:before, 287 | .devicon-bitbucket-plain:before { 288 | content: "\e90f"; 289 | } 290 | .devicon-safari-line-wordmark:before { 291 | content: "\e632"; 292 | } 293 | .devicon-safari-line:before { 294 | content: "\e63a"; 295 | } 296 | .devicon-safari-plain-wordmark:before { 297 | content: "\e63b"; 298 | } 299 | .devicon-safari-plain:before { 300 | content: "\e63c"; 301 | } 302 | .devicon-jetbrains-plain:before, 303 | .devicon-jetbrains-line:before, 304 | .devicon-jetbrains-line-wordmark:before, 305 | .devicon-jetbrains-plain-wordmark:before { 306 | content: "\e63d"; 307 | } 308 | .devicon-django-line:before, 309 | .devicon-django-line-wordmark:before { 310 | content: "\e63e"; 311 | } 312 | .devicon-django-plain:before, 313 | .devicon-django-plain-wordmark:before { 314 | content: "\e63f"; 315 | } 316 | 317 | .devicon-gimp-plain:before { 318 | content: "\e633"; 319 | } 320 | 321 | .devicon-redhat-plain-wordmark:before { 322 | content: "\e62a"; 323 | } 324 | 325 | .devicon-redhat-plain:before { 326 | content: "\e62b"; 327 | } 328 | 329 | .devicon-cplusplus-line:before, 330 | .devicon-cplusplus-line-wordmark:before { 331 | content: "\e634"; 332 | } 333 | 334 | .devicon-cplusplus-plain:before, 335 | .devicon-cplusplus-plain-wordmark:before { 336 | content: "\e635"; 337 | } 338 | 339 | .devicon-csharp-line:before, 340 | .devicon-csharp-line-wordmark:before { 341 | content: "\e636"; 342 | } 343 | 344 | .devicon-csharp-plain:before, 345 | .devicon-csharp-plain-wordmark:before { 346 | content: "\e637"; 347 | } 348 | 349 | .devicon-c-line:before, 350 | .devicon-c-line-wordmark:before { 351 | content: "\e638"; 352 | } 353 | 354 | .devicon-c-plain:before, 355 | .devicon-c-plain-wordmark:before { 356 | content: "\e639"; 357 | } 358 | 359 | .devicon-nodewebkit-line-wordmark:before { 360 | content: "\e611"; 361 | } 362 | 363 | .devicon-nodewebkit-line:before { 364 | content: "\e612"; 365 | } 366 | 367 | .devicon-nodewebkit-plain-wordmark:before { 368 | content: "\e613"; 369 | } 370 | 371 | .devicon-nodewebkit-plain:before { 372 | content: "\e614"; 373 | } 374 | 375 | .devicon-nginx-original:before, 376 | .devicon-nginx-original-wordmark:before, 377 | .devicon-nginx-plain:before, 378 | .devicon-nginx-plain-wordmark:before { 379 | content: "\e615"; 380 | } 381 | 382 | .devicon-erlang-plain-wordmark:before { 383 | content: "\e616"; 384 | } 385 | 386 | .devicon-erlang-plain:before { 387 | content: "\e617"; 388 | } 389 | 390 | .devicon-doctrine-line-wordmark:before { 391 | content: "\e618"; 392 | } 393 | 394 | .devicon-doctrine-line:before { 395 | content: "\e619"; 396 | } 397 | 398 | .devicon-doctrine-plain-wordmark:before { 399 | content: "\e61a"; 400 | } 401 | 402 | .devicon-doctrine-plain:before { 403 | content: "\e625"; 404 | } 405 | 406 | .devicon-apache-line-wordmark:before { 407 | content: "\e626"; 408 | } 409 | 410 | .devicon-apache-line:before { 411 | content: "\e627"; 412 | } 413 | 414 | .devicon-apache-plain-wordmark:before { 415 | content: "\e628"; 416 | } 417 | 418 | .devicon-apache-plain:before { 419 | content: "\e629"; 420 | } 421 | 422 | .devicon-go-line:before { 423 | content: "\e610"; 424 | } 425 | 426 | .devicon-redis-plain-wordmark:before { 427 | content: "\e606"; 428 | } 429 | 430 | .devicon-redis-plain:before { 431 | content: "\e607"; 432 | } 433 | 434 | .devicon-meteor-plain-wordmark:before { 435 | content: "\e608"; 436 | } 437 | 438 | .devicon-meteor-plain:before { 439 | content: "\e609"; 440 | } 441 | 442 | .devicon-heroku-line-wordmark:before, 443 | .devicon-heroku-original-wordmark:before { 444 | content: "\e60a"; 445 | } 446 | 447 | .devicon-heroku-line:before, 448 | .devicon-heroku-original:before { 449 | content: "\e60b"; 450 | } 451 | 452 | .devicon-heroku-plain-wordmark:before { 453 | content: "\e60c"; 454 | } 455 | 456 | .devicon-heroku-plain:before { 457 | content: "\e60f"; 458 | } 459 | 460 | .devicon-go-plain:before { 461 | content: "\e61b"; 462 | } 463 | 464 | .devicon-docker-plain-wordmark:before { 465 | content: "\e61e"; 466 | } 467 | 468 | .devicon-docker-plain:before { 469 | content: "\e61f"; 470 | } 471 | 472 | .devicon-amazonwebservices-original:before, 473 | .devicon-amazonwebservices-plain:before { 474 | content: "\e603"; 475 | } 476 | 477 | .devicon-amazonwebservices-plain-wordmark:before { 478 | content: "\e604"; 479 | } 480 | 481 | .devicon-android-plain-wordmark:before { 482 | content: "\e60d"; 483 | } 484 | 485 | .devicon-android-plain:before { 486 | content: "\e60e"; 487 | } 488 | 489 | .devicon-angularjs-plain-wordmark:before { 490 | content: "\e61c"; 491 | } 492 | 493 | .devicon-angularjs-plain:before { 494 | content: "\e61d"; 495 | } 496 | 497 | .devicon-appcelerator-original:before, 498 | .devicon-appcelerator-plain:before { 499 | content: "\e620"; 500 | } 501 | 502 | .devicon-appcelerator-plain-wordmark:before { 503 | content: "\e621"; 504 | } 505 | 506 | .devicon-apple-original:before, 507 | .devicon-apple-plain:before { 508 | content: "\e622"; 509 | } 510 | 511 | .devicon-atom-original-wordmark:before, 512 | .devicon-atom-plain-wordmark:before { 513 | content: "\e623"; 514 | } 515 | 516 | .devicon-atom-original:before, 517 | .devicon-atom-plain:before { 518 | content: "\e624"; 519 | } 520 | 521 | .devicon-backbonejs-plain-wordmark:before { 522 | content: "\e62c"; 523 | } 524 | 525 | .devicon-backbonejs-plain:before { 526 | content: "\e62d"; 527 | } 528 | 529 | .devicon-bootstrap-plain-wordmark:before { 530 | content: "\e62e"; 531 | } 532 | 533 | .devicon-bootstrap-plain:before { 534 | content: "\e62f"; 535 | } 536 | 537 | .devicon-bower-line-wordmark:before { 538 | content: "\e630"; 539 | } 540 | 541 | .devicon-bower-line:before { 542 | content: "\e631"; 543 | } 544 | 545 | .devicon-bower-plain-wordmark:before { 546 | content: "\e64e"; 547 | } 548 | 549 | .devicon-bower-plain:before { 550 | content: "\e64f"; 551 | } 552 | 553 | .devicon-chrome-plain-wordmark:before { 554 | content: "\e665"; 555 | } 556 | 557 | .devicon-chrome-plain:before { 558 | content: "\e666"; 559 | } 560 | 561 | .devicon-codeigniter-plain-wordmark:before { 562 | content: "\e667"; 563 | } 564 | 565 | .devicon-codeigniter-plain:before { 566 | content: "\e668"; 567 | } 568 | 569 | .devicon-coffeescript-original-wordmark:before, 570 | .devicon-coffeescript-plain-wordmark:before { 571 | content: "\e669"; 572 | } 573 | 574 | .devicon-coffeescript-original:before, 575 | .devicon-coffeescript-plain:before { 576 | content: "\e66a"; 577 | } 578 | 579 | .devicon-css3-plain-wordmark:before { 580 | content: "\e678"; 581 | } 582 | 583 | .devicon-css3-plain:before { 584 | content: "\e679"; 585 | } 586 | 587 | .devicon-debian-plain-wordmark:before { 588 | content: "\e67e"; 589 | } 590 | 591 | .devicon-debian-plain:before { 592 | content: "\e67f"; 593 | } 594 | 595 | .devicon-dot-net-plain-wordmark:before { 596 | content: "\e6d3"; 597 | } 598 | 599 | .devicon-dot-net-plain:before { 600 | content: "\e6d4"; 601 | } 602 | 603 | .devicon-drupal-plain-wordmark:before { 604 | content: "\e6e2"; 605 | } 606 | 607 | .devicon-drupal-plain:before { 608 | content: "\e6e3"; 609 | } 610 | 611 | .devicon-firefox-plain-wordmark:before { 612 | content: "\e75d"; 613 | } 614 | 615 | .devicon-firefox-plain:before { 616 | content: "\e75e"; 617 | } 618 | 619 | .devicon-foundation-plain-wordmark:before { 620 | content: "\e7a2"; 621 | } 622 | 623 | .devicon-foundation-plain:before { 624 | content: "\e7a3"; 625 | } 626 | 627 | .devicon-git-plain-wordmark:before { 628 | content: "\e7a7"; 629 | } 630 | 631 | .devicon-git-plain:before { 632 | content: "\e7a8"; 633 | } 634 | 635 | .devicon-google-original-wordmark:before, 636 | .devicon-google-plain-wordmark:before { 637 | content: "\e91d"; 638 | } 639 | 640 | .devicon-google-original:before, 641 | .devicon-google-plain:before { 642 | content: "\e91e"; 643 | } 644 | 645 | .devicon-grunt-line-wordmark:before { 646 | content: "\e7a9"; 647 | } 648 | 649 | .devicon-grunt-line:before { 650 | content: "\e7aa"; 651 | } 652 | 653 | .devicon-grunt-plain-wordmark:before { 654 | content: "\e7ea"; 655 | } 656 | 657 | .devicon-grunt-plain:before { 658 | content: "\e7eb"; 659 | } 660 | 661 | .devicon-gulp-plain:before { 662 | content: "\e7ec"; 663 | } 664 | 665 | .devicon-html5-plain-wordmark:before { 666 | content: "\e7f6"; 667 | } 668 | 669 | .devicon-html5-plain:before { 670 | content: "\e7f7"; 671 | } 672 | 673 | .devicon-ie10-original:before, 674 | .devicon-ie10-plain:before { 675 | content: "\e7f8"; 676 | } 677 | 678 | .devicon-illustrator-line:before { 679 | content: "\e7f9"; 680 | } 681 | 682 | .devicon-illustrator-plain:before { 683 | content: "\e7fa"; 684 | } 685 | 686 | .devicon-inkscape-plain-wordmark:before { 687 | content: "\e834"; 688 | } 689 | 690 | .devicon-inkscape-plain:before { 691 | content: "\e835"; 692 | } 693 | 694 | .devicon-java-plain-wordmark:before { 695 | content: "\e841"; 696 | } 697 | 698 | .devicon-java-plain:before { 699 | content: "\e842"; 700 | } 701 | 702 | .devicon-javascript-plain:before { 703 | content: "\e845"; 704 | } 705 | 706 | .devicon-jquery-plain-wordmark:before { 707 | content: "\e849"; 708 | } 709 | 710 | .devicon-jquery-plain:before { 711 | content: "\e84a"; 712 | } 713 | 714 | .devicon-krakenjs-plain-wordmark:before { 715 | content: "\e84f"; 716 | } 717 | 718 | .devicon-krakenjs-plain:before { 719 | content: "\e850"; 720 | } 721 | 722 | .devicon-laravel-plain-wordmark:before { 723 | content: "\e851"; 724 | } 725 | 726 | .devicon-laravel-plain:before { 727 | content: "\e852"; 728 | } 729 | 730 | .devicon-less-plain-wordmark:before { 731 | content: "\e853"; 732 | } 733 | 734 | .devicon-linux-plain:before { 735 | content: "\eb1c"; 736 | } 737 | 738 | .devicon-mongodb-plain-wordmark:before { 739 | content: "\eb43"; 740 | } 741 | 742 | .devicon-mongodb-plain:before { 743 | content: "\eb44"; 744 | } 745 | 746 | .devicon-moodle-plain-wordmark:before { 747 | content: "\eb5a"; 748 | } 749 | 750 | .devicon-moodle-plain:before { 751 | content: "\eb5b"; 752 | } 753 | 754 | .devicon-mysql-plain-wordmark:before { 755 | content: "\eb60"; 756 | } 757 | 758 | .devicon-mysql-plain:before { 759 | content: "\eb61"; 760 | } 761 | 762 | .devicon-nodejs-plain-wordmark:before { 763 | content: "\eb69"; 764 | } 765 | 766 | .devicon-nodejs-plain:before { 767 | content: "\eb6a"; 768 | } 769 | 770 | .devicon-oracle-original:before, 771 | .devicon-oracle-plain:before { 772 | content: "\eb6b"; 773 | } 774 | 775 | .devicon-photoshop-line:before { 776 | content: "\eb6c"; 777 | } 778 | 779 | .devicon-photoshop-plain:before { 780 | content: "\eb6d"; 781 | } 782 | 783 | .devicon-php-plain:before { 784 | content: "\eb71"; 785 | } 786 | 787 | .devicon-postgresql-plain-wordmark:before { 788 | content: "\eb7c"; 789 | } 790 | 791 | .devicon-postgresql-plain:before { 792 | content: "\eb7d"; 793 | } 794 | 795 | .devicon-python-plain-wordmark:before { 796 | content: "\eb88"; 797 | } 798 | 799 | .devicon-python-plain:before { 800 | content: "\eb89"; 801 | } 802 | 803 | .devicon-rails-plain-wordmark:before { 804 | content: "\eba2"; 805 | } 806 | 807 | .devicon-rails-plain:before { 808 | content: "\eba3"; 809 | } 810 | 811 | .devicon-react-original-wordmark:before, 812 | .devicon-react-plain-wordmark:before { 813 | content: "\e600"; 814 | } 815 | 816 | .devicon-react-original:before, 817 | .devicon-react-plain:before { 818 | content: "\e601"; 819 | } 820 | 821 | .devicon-ruby-plain-wordmark:before { 822 | content: "\ebc9"; 823 | } 824 | 825 | .devicon-ruby-plain:before { 826 | content: "\ebca"; 827 | } 828 | 829 | .devicon-sass-original:before, 830 | .devicon-sass-plain:before { 831 | content: "\ebcb"; 832 | } 833 | 834 | .devicon-sequelize-original-wordmark:before, 835 | .devicon-sequelize-plain-wordmark:before { 836 | content: "\e922"; 837 | } 838 | 839 | .devicon-sequelize-original:before, 840 | .devicon-sequelize-plain:before { 841 | content: "\e923"; 842 | } 843 | 844 | .devicon-symfony-original-wordmark:before, 845 | .devicon-symfony-plain-wordmark:before { 846 | content: "\e602"; 847 | } 848 | 849 | .devicon-symfony-original:before, 850 | .devicon-symfony-plain:before { 851 | content: "\e605"; 852 | } 853 | 854 | .devicon-travis-plain-wordmark:before { 855 | content: "\ebcc"; 856 | } 857 | 858 | .devicon-travis-plain:before { 859 | content: "\ebcd"; 860 | } 861 | 862 | .devicon-trello-plain-wordmark:before { 863 | content: "\ebce"; 864 | } 865 | 866 | .devicon-trello-plain:before { 867 | content: "\ebcf"; 868 | } 869 | 870 | .devicon-twitter-original:before, 871 | .devicon-twitter-plain:before { 872 | content: "\e91f"; 873 | } 874 | 875 | .devicon-ubuntu-plain-wordmark:before { 876 | content: "\ebd0"; 877 | } 878 | 879 | .devicon-ubuntu-plain:before { 880 | content: "\ebd1"; 881 | } 882 | 883 | .devicon-vim-plain:before { 884 | content: "\ebf3"; 885 | } 886 | 887 | .devicon-windows8-original-wordmark:before, 888 | .devicon-windows8-plain-wordmark:before { 889 | content: "\ebf4"; 890 | } 891 | 892 | .devicon-windows8-original:before, 893 | .devicon-windows8-plain:before { 894 | content: "\ebf5"; 895 | } 896 | 897 | .devicon-wordpress-plain-wordmark:before { 898 | content: "\ebfd"; 899 | } 900 | 901 | .devicon-wordpress-plain:before { 902 | content: "\ebfe"; 903 | } 904 | 905 | .devicon-yii-plain-wordmark:before { 906 | content: "\ec01"; 907 | } 908 | 909 | .devicon-yii-plain:before { 910 | content: "\ec02"; 911 | } 912 | 913 | .devicon-zend-plain-wordmark:before { 914 | content: "\ec03"; 915 | } 916 | 917 | .devicon-zend-plain:before { 918 | content: "\ec04"; 919 | } 920 | -------------------------------------------------------------------------------- /frontend/src/assets/example.json: -------------------------------------------------------------------------------- 1 | {"data":{"cert_index":343999487,"cert_link":"http://ct.googleapis.com/logs/argon2020/ct/v1/get-entries?start=343999487&end=343999487","chain":[{"extensions":{"authorityInfoAccess":"CA Issuers - URI:http://apps.identrust.com/roots/dstrootcax3.p7c\nOCSP - URI:http://isrg.trustid.ocsp.identrust.com\n","authorityKeyIdentifier":"keyid:C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10\n","basicConstraints":"CA:TRUE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.root-x1.letsencrypt.org","crlDistributionPoints":"Full Name:\n URI:http://crl.identrust.com/DSTROOTCAX3CRL.crl","keyUsage":"Digital Signature, Key Cert Sign, C R L Sign","subjectKeyIdentifier":"A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1"},"fingerprint":"E6:A3:B4:5B:06:2D:50:9B:33:82:28:2D:19:6E:FE:97:D5:95:6C:CB","not_after":1615999246,"not_before":1458232846,"serial_number":"A0141420000015385736A0B85ECA708","subject":{"C":"US","CN":"Let's Encrypt Authority X3","L":null,"O":"Let's Encrypt","OU":null,"ST":null,"aggregated":"/C=US/CN=Let's Encrypt Authority X3/O=Let's Encrypt"}},{"extensions":{"basicConstraints":"CA:TRUE","keyUsage":"Key Cert Sign, C R L Sign","subjectKeyIdentifier":"C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10"},"fingerprint":"DA:C9:02:4F:54:D8:F6:DF:94:93:5F:B1:73:26:38:CA:6A:D7:7C:13","not_after":1633010475,"not_before":970348339,"serial_number":"44AFB080D6A327BA893039862EF8406B","subject":{"C":null,"CN":"DST Root CA X3","L":null,"O":"Digital Signature Trust Co.","OU":null,"ST":null,"aggregated":"/CN=DST Root CA X3/O=Digital Signature Trust Co."}}],"leaf_cert":{"all_domains":["access.smm-cheap.com","www.access.smm-cheap.com"],"extensions":{"authorityInfoAccess":"CA Issuers - URI:http://cert.int-x3.letsencrypt.org/\nOCSP - URI:http://ocsp.int-x3.letsencrypt.org\n","authorityKeyIdentifier":"keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1\n","basicConstraints":"CA:FALSE","certificatePolicies":"Policy: 1.3.6.1.4.1.44947.1.1.1\n CPS: http://cps.letsencrypt.org","ctlSignedCertificateTimestamp":"BIHxAO8AdgBep3P531bA57U2SH3QSeAyepGaDIShEhKEGHWWgXFFWAAAAXBHIXo_AAAEAwBHMEUCIAJzKm8r6y8DH86jiH3p3nvee06E51GMyFXk_tCBou_kAiEAtFTGmtfDCElSeSpZscF_WDLeIp2NjQvoyl_jQHR3u6cAdQCyHgXMi6LNiiBOh2b5K7mKJSBna9r6cOeySVMt74uQXgAAAXBHIXotAAAEAwBGMEQCIDZ9wja7HJtLqHppVV6QHdfzeBM1PBEid9bmz3x2eyGwAiByBwCZux2N8qpF4N5cPYm5CV6gQBSb4aMmO1v0UQHaQw==","extendedKeyUsage":"TLS Web server authentication, TLS Web client authentication","keyUsage":"Digital Signature, Key Encipherment","subjectAltName":"DNS:www.access.smm-cheap.com, DNS:access.smm-cheap.com","subjectKeyIdentifier":"71:23:2B:93:50:A0:60:11:EF:7A:29:9D:23:99:80:AF:06:68:40:FF"},"fingerprint":"84:E0:18:01:52:93:08:E0:BC:1C:DA:C8:B4:48:64:76:8A:61:56:32","not_after":1589513741,"not_before":1581737741,"serial_number":"37A3A72033B9AFF73A4BED5BB4F7CE6746E","subject":{"C":null,"CN":"access.smm-cheap.com","L":null,"O":null,"OU":null,"ST":null,"aggregated":"/CN=access.smm-cheap.com"}},"seen":1581741558.477939,"source":{"name":"Google 'Argon2020' log","url":"ct.googleapis.com/logs/argon2020/"},"update_type":"X509LogEntry"},"message_type":"certificate_update"} -------------------------------------------------------------------------------- /frontend/src/assets/fonts/devicon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/src/assets/fonts/devicon.eot -------------------------------------------------------------------------------- /frontend/src/assets/fonts/devicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/src/assets/fonts/devicon.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/devicon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/src/assets/fonts/devicon.woff -------------------------------------------------------------------------------- /frontend/src/assets/img/certstream-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/src/assets/img/certstream-bg.png -------------------------------------------------------------------------------- /frontend/src/assets/img/certstream-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/src/assets/img/certstream-overview.png -------------------------------------------------------------------------------- /frontend/src/assets/img/doghead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/src/assets/img/doghead.png -------------------------------------------------------------------------------- /frontend/src/assets/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/src/assets/img/favicon.png -------------------------------------------------------------------------------- /frontend/src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/src/assets/img/logo.png -------------------------------------------------------------------------------- /frontend/src/assets/img/rolling-transition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/src/assets/img/rolling-transition.png -------------------------------------------------------------------------------- /frontend/src/components/FeedWatcher.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 222 | 223 | 478 | -------------------------------------------------------------------------------- /frontend/src/components/Frontpage.vue: -------------------------------------------------------------------------------- 1 | 184 | 185 | 318 | 319 | 894 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | import Vue from 'vue' 3 | import App from './App' 4 | import router from './router' 5 | 6 | import VTooltip from 'v-tooltip' 7 | import VueScrollTo from 'vue-scrollto' 8 | 9 | Vue.use(VTooltip) 10 | Vue.use(VueScrollTo) 11 | 12 | Vue.config.productionTip = false 13 | 14 | /* eslint-disable no-new */ 15 | window.vueInstance = new Vue({ 16 | el: '#app', 17 | router, 18 | render: h => h(App) 19 | }) 20 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Frontpage from '@/components/Frontpage' 4 | 5 | Vue.use(Router) 6 | 7 | export default new Router({ 8 | mode: 'history', 9 | routes: [ 10 | { 11 | path: '/', 12 | name: 'Frontpage', 13 | component: Frontpage 14 | } 15 | ] 16 | }) 17 | -------------------------------------------------------------------------------- /frontend/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/static/.gitkeep -------------------------------------------------------------------------------- /frontend/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaliDog/certstream-server/60de7000901e5eb246d2e83c908678b43e5a60c8/frontend/static/favicon.png -------------------------------------------------------------------------------- /lib/certstream.ex: -------------------------------------------------------------------------------- 1 | defmodule Certstream do 2 | @moduledoc """ 3 | Certstream is a service for watching CT servers, parsing newly discovered certificates, 4 | and broadcasting updates to connected websocket clients. 5 | """ 6 | use Application 7 | 8 | def start(_type, _args) do 9 | children = [ 10 | # Web services 11 | Certstream.WebsocketServer, 12 | 13 | # Agents 14 | Certstream.ClientManager, 15 | Certstream.CertifcateBuffer, 16 | 17 | # Watchers 18 | {DynamicSupervisor, name: WatcherSupervisor, strategy: :one_for_one} 19 | ] 20 | 21 | supervisor_info = Supervisor.start_link(children, strategy: :one_for_one) 22 | 23 | Certstream.CTWatcher.start_and_link_watchers(name: WatcherSupervisor) 24 | 25 | supervisor_info 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/certstream/certificate_buffer_agent.ex: -------------------------------------------------------------------------------- 1 | require Logger 2 | 3 | defmodule Certstream.CertifcateBuffer do 4 | use Agent 5 | use Instruments 6 | 7 | @moduledoc """ 8 | An agent designed to ring-buffer certificate updates as they come in so the most recent 25 certificates can be 9 | aggregated for the /example.json and /latest.json routes. 10 | """ 11 | 12 | @doc "Starts the CertificateBuffer agent and creates an ETS table for tracking the certificates processed" 13 | def start_link(_opts) do 14 | Logger.info("Starting #{__MODULE__}...") 15 | Agent.start_link( 16 | fn -> 17 | :ets.new(:counter, [:named_table, :public]) 18 | :ets.insert(:counter, processed_certificates: 0) 19 | [] 20 | end, 21 | name: __MODULE__ 22 | ) 23 | end 24 | 25 | @doc "Adds a certificate update to the circular certificate buffer" 26 | def add_certs_to_buffer(certificates) do 27 | cert_count = length(certificates) 28 | 29 | :ets.update_counter(:counter, :processed_certificates, cert_count) 30 | Instruments.increment("certstream.all.processed_certificates", cert_count) 31 | 32 | Agent.update(__MODULE__, fn state -> 33 | state = certificates ++ state 34 | state |> Enum.take(25) 35 | end) 36 | end 37 | 38 | @doc "The number of certificates processed" 39 | def get_processed_certificates do 40 | :ets.lookup(:counter, :processed_certificates) 41 | |> Keyword.get(:processed_certificates) 42 | end 43 | 44 | @doc "Gets the latest certificate seen by Certstream, indented with 4 spaces" 45 | def get_example_json do 46 | Agent.get(__MODULE__, 47 | fn certificates -> 48 | certificates 49 | |> List.first 50 | |> Jason.encode!(pretty: true) 51 | end 52 | ) 53 | end 54 | 55 | @doc "Gets the latest 25 cetficiates seen by Certstream, indented with 4 spaces" 56 | def get_latest_json do 57 | Agent.get(__MODULE__, 58 | fn certificates -> 59 | %{} 60 | |> Map.put(:messages, certificates) 61 | |> Jason.encode!(pretty: true) 62 | end 63 | ) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/certstream/client_manager_agent.ex: -------------------------------------------------------------------------------- 1 | require Logger 2 | 3 | defmodule Certstream.ClientManager do 4 | @moduledoc """ 5 | An agent responsible for managing and broadcasting to websocket clients. Uses :pobox to 6 | provide buffering and eventually drops messages if the backpressure isn't enough. 7 | """ 8 | use Agent 9 | 10 | @full_stream_url Application.fetch_env!(:certstream, :full_stream_url) 11 | @domains_only_url Application.fetch_env!(:certstream, :domains_only_url) 12 | 13 | def start_link(_opts) do 14 | Logger.info("Starting #{__MODULE__}...") 15 | Agent.start_link(fn -> %{} end, name: __MODULE__) 16 | end 17 | 18 | def add_client(client_pid, client_state) do 19 | {:ok, box_pid} = :pobox.start_link(client_pid, 500, :queue) 20 | 21 | :pobox.active(box_pid, fn(msg, _) -> {{:ok, msg}, :nostate} end, :nostate) 22 | 23 | Agent.update( 24 | __MODULE__, 25 | &Map.put( 26 | &1, 27 | client_pid, 28 | client_state |> Map.put(:po_box, box_pid) 29 | ) 30 | ) 31 | end 32 | 33 | def remove_client(client_pid) do 34 | Agent.update(__MODULE__, fn state -> 35 | # Remove our pobox 36 | state |> Map.get(client_pid) |> Map.get(:po_box) |> Process.exit(:kill) 37 | 38 | # Remove client from state map 39 | state |> Map.delete(client_pid) 40 | end) 41 | end 42 | 43 | def get_clients do 44 | Agent.get(__MODULE__, fn state -> state end) 45 | end 46 | 47 | def get_client_count do 48 | Agent.get(__MODULE__, fn state -> state |> Map.keys |> length end) 49 | end 50 | 51 | def get_clients_json do 52 | Agent.get(__MODULE__, fn state -> 53 | 54 | state 55 | |> Enum.map(fn {k, v} -> 56 | coerced_payload = v 57 | |> Map.update!(:connect_time, &DateTime.to_iso8601/1) 58 | |> Map.drop([:po_box, :is_websocket]) 59 | {inspect(k), coerced_payload} 60 | end) 61 | |> Enum.into(%{}) 62 | end) 63 | end 64 | 65 | def broadcast_to_clients(entries) do 66 | Logger.debug(fn -> "Broadcasting #{length(entries)} certificates to clients" end) 67 | 68 | certificates = entries 69 | |> Enum.map(&(%{:message_type => "certificate_update", :data => &1})) 70 | 71 | serialized_certificates_full = certificates 72 | |> Enum.map(&Jason.encode!/1) 73 | 74 | certificates_lite = certificates 75 | |> Enum.map(&remove_chain_from_cert/1) 76 | |> Enum.map(&remove_der_from_cert/1) 77 | 78 | Certstream.CertifcateBuffer.add_certs_to_buffer(certificates_lite) 79 | 80 | serialized_certificates_lite = certificates_lite 81 | |> Enum.map(&Jason.encode!/1) 82 | 83 | dns_entries_only = certificates 84 | |> Enum.map(&get_in(&1, [:data, :leaf_cert, :all_domains])) 85 | |> Enum.map(fn dns_entries -> %{:message_type => "dns_entries", :data => dns_entries} end) 86 | |> Enum.map(&Jason.encode!/1) 87 | 88 | get_clients() 89 | |> Enum.each(fn {_, client_state} -> 90 | case client_state.path do 91 | @full_stream_url -> send_bundle(serialized_certificates_full, client_state.po_box) 92 | @full_stream_url <> "/" -> send_bundle(serialized_certificates_full, client_state.po_box) 93 | @domains_only_url -> send_bundle(dns_entries_only, client_state.po_box) 94 | @domains_only_url <> "/" -> send_bundle(dns_entries_only, client_state.po_box) 95 | _ -> send_bundle(serialized_certificates_lite, client_state.po_box) 96 | end 97 | end) 98 | end 99 | 100 | def send_bundle(entries, po_box) do 101 | :pobox.post(po_box, entries) 102 | end 103 | 104 | def remove_chain_from_cert(cert) do 105 | cert 106 | |> pop_in([:data, :chain]) 107 | |> elem(1) 108 | end 109 | 110 | def remove_der_from_cert(cert) do 111 | # Clean the der field from the leaf cert 112 | cert 113 | |> pop_in([:data, :leaf_cert, :as_der]) 114 | |> elem(1) 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/certstream/ct_parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Certstream.CTParser do 2 | @moduledoc false 3 | 4 | @log_entry_types %{ 5 | 0 => :X509LogEntry, 6 | 1 => :PrecertLogEntry 7 | } 8 | 9 | def parse_entry(entry) do 10 | leaf_input = Base.decode64!(entry["leaf_input"]) 11 | extra_data = Base.decode64!(entry["extra_data"]) 12 | 13 | << 14 | _version :: integer, 15 | _leaf_type :: integer, 16 | _timestamp :: size(64), 17 | type :: size(16), 18 | entry :: binary 19 | >> = leaf_input 20 | 21 | entry_type = @log_entry_types[type] 22 | 23 | cert = %{:update_type => entry_type} 24 | 25 | [top | rest] = [ 26 | parse_leaf_entry(entry_type, entry), 27 | parse_extra_data(entry_type, extra_data) 28 | ] |> List.flatten 29 | 30 | cert 31 | |> Map.put(:leaf_cert, top) 32 | |> Map.put(:chain, rest) 33 | 34 | end 35 | 36 | defp parse_extra_data(:X509LogEntry, extra_data) do 37 | <<_chain_length :: size(24), chain::binary>> = extra_data 38 | parse_certificate_chain(chain, []) 39 | end 40 | 41 | defp parse_extra_data(:PrecertLogEntry, extra_data) do 42 | << 43 | length :: size(24), 44 | certificate_data :: binary - size(length), 45 | _chain_length :: size(24), 46 | extra_chain :: binary 47 | >> = extra_data 48 | 49 | [ 50 | parse_certificate(certificate_data, :leaf), 51 | parse_certificate_chain(extra_chain, []) 52 | ] 53 | 54 | end 55 | 56 | defp parse_certificate(certificate_data, type) do 57 | case type do 58 | :leaf -> EasySSL.parse_der(certificate_data, serialize: true, all_domains: true) 59 | :chain -> EasySSL.parse_der(certificate_data, serialize: true) 60 | end 61 | 62 | end 63 | 64 | defp parse_certificate_chain(<>, entries) do 65 | parse_certificate_chain(rest, [parse_certificate(certificate_data, :chain) | entries]) 66 | end 67 | 68 | defp parse_certificate_chain(<<>>, entries) do 69 | entries |> Enum.reverse() 70 | end 71 | 72 | defp parse_leaf_entry(:X509LogEntry, 73 | <>) 76 | do 77 | 78 | parse_certificate(certificate_data, :leaf) 79 | end 80 | 81 | defp parse_leaf_entry(:PrecertLogEntry, _entry) do [] end # For now we don't parse these and rely on everything in "extra_data" only 82 | 83 | end 84 | -------------------------------------------------------------------------------- /lib/certstream/ct_watcher.ex: -------------------------------------------------------------------------------- 1 | require Logger 2 | 3 | defmodule Certstream.CTWatcher do 4 | @moduledoc """ 5 | The GenServer responsible for watching a specific CT server. It ticks every 15 seconds via 6 | `schedule_update`, and uses Process.send_after to trigger new requests to see if there are 7 | any certificates to fetch and broadcast. 8 | """ 9 | use GenServer 10 | use Instruments 11 | 12 | @default_http_options [timeout: 10_000, recv_timeout: 10_000, ssl: [{:versions, [:'tlsv1.2']}], follow_redirect: true] 13 | 14 | def child_spec(log) do 15 | %{ 16 | id: __MODULE__, 17 | start: {__MODULE__, :start_link, [log]}, 18 | restart: :permanent, 19 | } 20 | end 21 | 22 | def start_and_link_watchers(name: supervisor_name) do 23 | Logger.info("Initializing CT Watchers...") 24 | # Fetch all CT lists 25 | ctl_log_info = "https://www.gstatic.com/ct/log_list/v3/all_logs_list.json" 26 | |> HTTPoison.get!([], @default_http_options) 27 | |> Map.get(:body) 28 | |> Jason.decode! 29 | 30 | 31 | ctl_log_info 32 | |> Map.get("operators") 33 | |> Enum.each(fn operator -> 34 | operator 35 | |> Map.get("logs") 36 | |> Enum.each(fn log -> 37 | log = Map.put(log, "operator_name", operator["name"]) 38 | DynamicSupervisor.start_child(supervisor_name, child_spec(log)) 39 | end) 40 | end) 41 | end 42 | 43 | def start_link(log) do 44 | GenServer.start_link( 45 | __MODULE__, 46 | %{:operator => log, :url => log["url"]} 47 | ) 48 | end 49 | 50 | def init(state) do 51 | # Schedule the initial update to happen between 0 and 3 seconds from now in 52 | # order to stagger when we hit these servers and avoid a thundering herd sort 53 | # of issue upstream 54 | delay = :rand.uniform(30) / 10 55 | 56 | Logger.info("Worker #{inspect self()} started with url #{state[:url]} and initial start time of #{delay} seconds from now.") 57 | 58 | send(self(), :init) 59 | 60 | {:ok, state} 61 | end 62 | 63 | def http_request_with_retries(full_url, options \\ @default_http_options) do 64 | # Go ask for the first 512 entries 65 | Logger.info("Sending GET request to #{full_url}") 66 | 67 | user_agent = {"User-Agent", user_agent()} 68 | 69 | case HTTPoison.get(full_url, [user_agent], options) do 70 | {:ok, %HTTPoison.Response{status_code: 200} = response} -> 71 | response.body 72 | |> Jason.decode! 73 | 74 | {:ok, response} -> 75 | Logger.error("Unexpected status code #{response.status_code} fetching url #{full_url}! Sleeping for a bit and trying again...") 76 | :timer.sleep(:timer.seconds(10)) 77 | http_request_with_retries(full_url, options) 78 | 79 | {:error, %HTTPoison.Error{reason: reason}} -> 80 | Logger.error("Error: #{inspect reason} while GETing #{full_url}! Sleeping for 10 seconds and trying again...") 81 | :timer.sleep(:timer.seconds(10)) 82 | http_request_with_retries(full_url, options) 83 | end 84 | end 85 | 86 | def get_tree_size(state) do 87 | "#{state[:url]}ct/v1/get-sth" 88 | |> http_request_with_retries 89 | |> Map.get("tree_size") 90 | end 91 | 92 | def handle_info({:ssl_closed, _}, state) do 93 | Logger.info("Worker #{inspect self()} got :ssl_closed message. Ignoring.") 94 | {:noreply, state} 95 | end 96 | 97 | def handle_info(:init, state) do 98 | # On first run attempt to fetch 512 certificates, and see what the API returns. However 99 | # many certs come back is what we should use as the batch size moving forward (at least 100 | # in theory). 101 | 102 | state = 103 | try do 104 | batch_size = "#{state[:url]}ct/v1/get-entries?start=0&end=511" 105 | |> HTTPoison.get!([], @default_http_options) 106 | |> Map.get(:body) 107 | |> Jason.decode! 108 | |> Map.get("entries") 109 | |> Enum.count 110 | 111 | Logger.info("Worker #{inspect self()} with url #{state[:url]} found batch size of #{batch_size}.") 112 | 113 | state = Map.put(state, :batch_size, batch_size) 114 | 115 | # On first run populate the state[:tree_size] key 116 | state = Map.put(state, :tree_size, get_tree_size(state)) 117 | 118 | send(self(), :update) 119 | 120 | state 121 | rescue e -> 122 | Logger.warn("Worker #{inspect self()} with state #{inspect state} blew up because #{inspect e}") 123 | end 124 | 125 | {:noreply, state} 126 | end 127 | 128 | def handle_info(:update, state) do 129 | Logger.debug(fn -> "Worker #{inspect self()} got tick." end) 130 | 131 | current_tree_size = get_tree_size(state) 132 | 133 | Logger.debug(fn -> "Tree size #{current_tree_size} - #{state[:tree_size]}" end) 134 | 135 | state = case current_tree_size > state[:tree_size] do 136 | true -> 137 | Logger.info("Worker #{inspect self()} with url #{state[:url]} found #{current_tree_size - state[:tree_size]} certificates [#{state[:tree_size]} -> #{current_tree_size}].") 138 | 139 | cert_count = current_tree_size - state[:tree_size] 140 | Instruments.increment("certstream.worker", cert_count, tags: ["url:#{state[:url]}"]) 141 | Instruments.increment("certstream.aggregate_owners_count", cert_count, tags: [~s(owner:#{state[:operator]["operator_name"]})]) 142 | 143 | broadcast_updates(state, current_tree_size) 144 | 145 | state 146 | |> Map.put(:tree_size, current_tree_size) 147 | |> Map.update(:processed_count, 0, &(&1 + (current_tree_size - state[:tree_size]))) 148 | false -> state 149 | end 150 | 151 | schedule_update() 152 | 153 | {:noreply, state} 154 | end 155 | 156 | defp broadcast_updates(state, current_size) do 157 | certificate_count = (current_size - state[:tree_size]) 158 | certificates = Enum.to_list (current_size - certificate_count)..current_size - 1 159 | 160 | Logger.info("Certificate count - #{certificate_count} ") 161 | certificates 162 | |> Enum.chunk_every(state[:batch_size]) 163 | # Use Task.async_stream to have 5 concurrent requests to the CT server to fetch 164 | # our certificates without waiting on the previous chunk. 165 | |> Task.async_stream(&(fetch_and_broadcast_certs(&1, state)), max_concurrency: 5, timeout: :timer.seconds(600)) 166 | |> Enum.to_list # Nop to just pull the requests through async_stream 167 | end 168 | 169 | def fetch_and_broadcast_certs(ids, state) do 170 | Logger.debug(fn -> "Attempting to retrieve #{ids |> Enum.count} entries" end) 171 | entries = "#{state[:url]}ct/v1/get-entries?start=#{List.first(ids)}&end=#{List.last(ids)}" 172 | |> http_request_with_retries 173 | |> Map.get("entries", []) 174 | 175 | entries 176 | |> Enum.zip(ids) 177 | |> Enum.map(fn {entry, cert_index} -> 178 | entry 179 | |> Certstream.CTParser.parse_entry 180 | |> Map.merge( 181 | %{ 182 | :cert_index => cert_index, 183 | :seen => :os.system_time(:microsecond) / 1_000_000, 184 | :source => %{ 185 | :url => state[:operator]["url"], 186 | :name => state[:operator]["description"], 187 | }, 188 | :cert_link => "#{state[:operator]["url"]}ct/v1/get-entries?start=#{cert_index}&end=#{cert_index}" 189 | } 190 | ) 191 | end) 192 | |> Certstream.ClientManager.broadcast_to_clients 193 | 194 | entry_count = Enum.count(entries) 195 | batch_count = Enum.count(ids) 196 | 197 | # If we have *unequal* counts the API has returned less certificates than our initial batch 198 | # heuristic. Drop the entires we retrieved and recurse to fetch others. 199 | if entry_count != batch_count do 200 | Logger.debug(fn -> 201 | "We didn't retrieve all the entries for this batch, fetching missing #{batch_count - entry_count} entries" 202 | end) 203 | fetch_and_broadcast_certs(ids |> Enum.drop(Enum.count(entries)), state) 204 | end 205 | end 206 | 207 | defp schedule_update(seconds \\ 10) do # Default to 10 second ticks 208 | # Note, we need to use Kernel.trunc() here to guarentee this is an integer 209 | # because :timer.seconds returns an integer or a float depending on the 210 | # type put in, :erlang.send_after seems to hang with floats for some 211 | # reason :( 212 | Process.send_after(self(), :update, trunc(:timer.seconds(seconds))) 213 | end 214 | 215 | # Allow the user agent to be overridden in the config, or use a default Certstream identifier 216 | defp user_agent do 217 | case Application.fetch_env!(:certstream, :user_agent) do 218 | :default -> "Certstream Server v#{Application.spec(:certstream, :vsn)}" 219 | user_agent_override -> user_agent_override 220 | end 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /lib/certstream/web.ex: -------------------------------------------------------------------------------- 1 | require Logger 2 | 3 | defmodule Certstream.WebsocketServer do 4 | @moduledoc """ 5 | The main web services GenServer, responsible for spinning up cowboy and encapsulates 6 | all logic for web routes/websockets. 7 | """ 8 | use GenServer 9 | use Instruments 10 | 11 | 12 | @full_stream_url Application.fetch_env!(:certstream, :full_stream_url) 13 | @domains_only_url Application.fetch_env!(:certstream, :domains_only_url) 14 | 15 | # GenServer callback 16 | def init(args) do {:ok, args} end 17 | 18 | # /example.json handler 19 | def init(req, [:example_json]) do 20 | res = :cowboy_req.reply(200, %{'content-type' => 'application/json'}, 21 | Certstream.CertifcateBuffer.get_example_json(), req) 22 | {:ok, res, %{}} 23 | end 24 | 25 | # /latest.json handler 26 | def init(req, [:latest_json]) do 27 | res = :cowboy_req.reply(200, %{'content-type' => 'application/json'}, 28 | Certstream.CertifcateBuffer.get_latest_json(), req) 29 | {:ok, res, %{}} 30 | end 31 | 32 | # /stats handler 33 | def init(req, [:stats]) do 34 | processed_certs = Certstream.CertifcateBuffer.get_processed_certificates 35 | client_json = Certstream.ClientManager.get_clients_json 36 | 37 | workers = WatcherSupervisor 38 | |> DynamicSupervisor.which_children 39 | |> Enum.reduce(%{}, fn {:undefined, pid, :worker, _module}, acc -> 40 | state = :sys.get_state pid 41 | Map.put(acc, state[:url], state[:processed_count] || 0) 42 | end) 43 | 44 | response = %{} 45 | |> Map.put(:processed_certificates, processed_certs) 46 | |> Map.put(:current_users, client_json) 47 | |> Map.put(:workers, workers) 48 | |> Jason.encode! 49 | |> Jason.Formatter.pretty_print 50 | 51 | res = :cowboy_req.reply( 52 | 200, 53 | %{'content-type' => 'application/json'}, 54 | response, 55 | req 56 | ) 57 | {:ok, res, %{}} 58 | end 59 | 60 | # / handler 61 | def init(req, state) do 62 | # If we have a websocket request, do the thing, otherwise just host our main HTML 63 | if Map.has_key?(req.headers, "upgrade") do 64 | Logger.debug(fn -> "New client connected #{inspect req.peer}" end) 65 | { 66 | :cowboy_websocket, 67 | req, 68 | %{ 69 | :is_websocket => true, 70 | :connect_time => DateTime.utc_now, 71 | :ip_address => req.peer |> elem(0) |> :inet_parse.ntoa |> to_string, 72 | :headers => req.headers, 73 | :path => req.path 74 | }, 75 | %{:compress => true} 76 | } 77 | else 78 | Instruments.increment("certstream.index_load", 1, tags: ["ip:#{state[:ip_address]}"]) 79 | res = :cowboy_req.reply( 80 | 200, 81 | %{'content-type' => 'text/html'}, 82 | File.read!("frontend/dist/index.html"), 83 | req 84 | ) 85 | {:ok, res, state} 86 | end 87 | end 88 | 89 | def terminate(_reason, _partial_req, state) do 90 | if state[:is_websocket] do 91 | Instruments.increment("certstream.websocket_disconnect", 1, tags: ["ip:#{state[:ip_address]}"]) 92 | Logger.debug(fn -> "Client disconnected #{inspect state.ip_address}" end) 93 | Certstream.ClientManager.remove_client(self()) 94 | end 95 | end 96 | 97 | def websocket_init(state) do 98 | Logger.info("Client connected #{inspect state.ip_address}") 99 | Instruments.increment("certstream.websocket_connect", 1, tags: ["ip:#{state[:ip_address]}"]) 100 | Certstream.ClientManager.add_client(self(), state) 101 | {:ok, state} 102 | end 103 | 104 | def websocket_handle(frame, state) do 105 | Logger.debug(fn -> "Client sent message #{inspect frame}" end) 106 | Instruments.increment("certstream.websocket_msg_in", 1, tags: ["ip:#{state[:ip_address]}"]) 107 | {:ok, state} 108 | end 109 | 110 | def websocket_info({:mail, box_pid, serialized_certificates, _message_count, message_drop_count}, state) do 111 | if message_drop_count > 0 do 112 | Instruments.increment("certstream.dropped_messages", message_drop_count, tags: ["ip:#{state[:ip_address]}"]) 113 | Logger.warn("Message drop count greater than 0 -> #{message_drop_count}") 114 | end 115 | 116 | Logger.debug(fn -> "Sending client #{length(serialized_certificates |> List.flatten)} client frames" end) 117 | 118 | # Reactive our pobox active mode 119 | :pobox.active(box_pid, fn(msg, _) -> {{:ok, msg}, :nostate} end, :nostate) 120 | 121 | response = serialized_certificates 122 | |> Enum.map(fn message -> 123 | message 124 | |> Enum.map(&({:text, &1})) 125 | end) 126 | |> List.flatten 127 | 128 | { 129 | :reply, 130 | response, 131 | state 132 | } 133 | end 134 | 135 | def start_link(_opts) do 136 | Logger.info("Starting web server on port #{get_port()}...") 137 | :cowboy.start_clear( 138 | :websocket_server, 139 | [{:port, get_port()}], 140 | %{ 141 | :env => %{ 142 | :dispatch => :cowboy_router.compile([ 143 | {:_, 144 | [ 145 | {"/", __MODULE__, []}, 146 | {@full_stream_url, __MODULE__, []}, 147 | {@domains_only_url, __MODULE__, []}, 148 | {"/example.json", __MODULE__, [:example_json]}, 149 | {"/latest.json", __MODULE__, [:latest_json]}, 150 | {"/static/[...]", :cowboy_static, {:dir, "frontend/dist/static/"}}, 151 | {"/#{System.get_env(~s(STATS_URL)) || 'stats'}", __MODULE__, [:stats]} 152 | ]} 153 | ]) 154 | }, 155 | } 156 | ) 157 | 158 | GenServer.start_link(__MODULE__, [], name: __MODULE__) 159 | end 160 | 161 | def child_spec(opts) do 162 | %{ 163 | id: __MODULE__, 164 | start: {__MODULE__, :start_link, [opts]}, 165 | restart: :permanent, 166 | name: __MODULE__ 167 | } 168 | end 169 | 170 | defp get_port do 171 | case System.get_env("PORT") do 172 | nil -> 4000 173 | port_string -> port_string |> Integer.parse |> elem(0) 174 | end 175 | end 176 | 177 | end 178 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Certstream.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :certstream, 7 | version: "1.6.0", 8 | elixir: "~> 1.6", 9 | start_permanent: Mix.env == :prod, 10 | deps: deps(), 11 | aliases: aliases(), 12 | test_coverage: [tool: ExCoveralls], 13 | preferred_cli_env: [coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test, "coveralls.html": :test] 14 | ] 15 | end 16 | 17 | def application do 18 | [ 19 | extra_applications: [:honeybadger, :logger], 20 | mod: {Certstream, []}, 21 | ] 22 | end 23 | 24 | defp deps do 25 | [ 26 | {:cowboy, "~> 2.8"}, 27 | {:easy_ssl, "~> 1.1"}, 28 | {:honeybadger, "~> 0.14"}, 29 | {:httpoison, "~> 1.7"}, 30 | {:instruments, "~> 1.1"}, 31 | {:jason, "~> 1.2"}, 32 | {:number, "~> 1.0"}, 33 | {:pobox, "~> 1.2"}, 34 | 35 | {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, 36 | {:excoveralls, "~> 0.13", only: :test} 37 | ] 38 | end 39 | 40 | defp aliases do 41 | [ 42 | test: "test --no-start" 43 | ] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, 4 | "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, 5 | "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, 6 | "credo": {:hex, :credo, "1.5.1", "4fe303cc828412b9d21eed4eab60914c401e71f117f40243266aafb66f30d036", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "0b219ca4dcc89e4e7bc6ae7e6539c313e738e192e10b85275fa1e82b5203ecd7"}, 7 | "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, 8 | "easy_ssl": {:hex, :easy_ssl, "1.3.0", "472256942d9dd37652a558a789a8d1cccc27e7f46352e32667d1ca46bb9e22e5", [:mix], [], "hexpm", "ce8fcb7661442713a94853282b56cee0b90c52b983a83aa6af24686d301808e1"}, 9 | "excoveralls": {:hex, :excoveralls, "0.13.3", "edc5f69218f84c2bf61b3609a22ddf1cec0fbf7d1ba79e59f4c16d42ea4347ed", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cc26f48d2f68666380b83d8aafda0fffc65dafcc8d8650358e0b61f6a99b1154"}, 10 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 11 | "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, 12 | "honeybadger": {:hex, :honeybadger, "0.15.0", "12059aabd7e30996d993d3132a6e4996e96f6fc66f14f4b6b7deb0683fe39696", [:mix], [{:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:hackney, "~> 1.1", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.0.0 and < 2.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f160b050e09bd9775e170a8832385bad45b3d202f9317d5c4f36b9da18584725"}, 13 | "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, 14 | "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, 15 | "instruments": {:hex, :instruments, "1.1.3", "c293e446ba6226db0cec94abd2a3cba025a743f79bfc5d9f810e5b4061fb7de0", [:mix], [{:recon, "~> 2.3.1", [hex: :recon, repo: "hexpm", optional: false]}, {:statix, "~> 1.2.1", [hex: :statix, repo: "hexpm", optional: false]}], "hexpm", "1502a9a1668d6b4056d8fe023c0006d50d0f032b6788cdc3b725bb44dd77d6f6"}, 16 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 17 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 18 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 19 | "number": {:hex, :number, "1.0.3", "932c8a2d478a181c624138958ca88a78070332191b8061717270d939778c9857", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "dd397bbc096b2ca965a6a430126cc9cf7b9ef7421130def69bcf572232ca0f18"}, 20 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, 21 | "pobox": {:hex, :pobox, "1.2.0", "3127cb48f13d18efec7a9ea2622077f4f9c5f067cc1182af1977dacd7a74fdb8", [:rebar3], [], "hexpm", "25d6fcdbe4fedbbf4bcaa459fadee006e75bb3281d4e6c9b2dc0ee93c51920c4"}, 22 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, 23 | "recon": {:hex, :recon, "2.3.6", "2bcad0cf621fb277cabbb6413159cd3aa30265c2dee42c968697988b30108604", [:rebar3], [], "hexpm", "f55198650a8ec01d3efc04797abe550c7d023e7ff8b509f373cf933032049bd8"}, 24 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 25 | "statix": {:hex, :statix, "1.2.1", "4f23c8cc2477ea0de89fed5e34f08c54b0d28b838f7b8f26613155f2221bb31e", [:mix], [], "hexpm", "7f988988fddcce19ae376bb8e47aa5ea5dabf8d4ba78d34d1ae61eb537daf72e"}, 26 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 27 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, 28 | } 29 | -------------------------------------------------------------------------------- /test/cert_buffer_agent_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CertifcateBufferTest do 2 | use ExUnit.Case 3 | 4 | test "ETS table is created and destroyed properly" do 5 | {:ok, buffer_pid} = Certstream.CertifcateBuffer.start_link([]) 6 | Process.unlink(buffer_pid) 7 | 8 | ref = Process.monitor(buffer_pid) 9 | 10 | # Assert ets table creation 11 | assert :ets.info(:counter) != :undefined 12 | 13 | # Kill process 14 | Process.exit(buffer_pid, :kill) 15 | 16 | # Wait for the process to die 17 | receive do 18 | {:DOWN, ^ref, _, _, _} -> 19 | # Ensure ets table is cleaned up properly upon agent termination 20 | assert :ets.info(:counter) == :undefined 21 | end 22 | 23 | # Start agent again 24 | {:ok, buffer_pid} = Certstream.CertifcateBuffer.start_link([]) 25 | Process.unlink(buffer_pid) 26 | 27 | # Assert it re-created the table 28 | assert :ets.info(:counter) != :undefined 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/ctl_parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CtlParserTest do 2 | use ExUnit.Case 3 | @standard_entry %{ 4 | "extra_data" => "AAfqAASWMIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0NlowSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMTGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EFq6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWAa6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIGCCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNvbTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9kc3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAwVAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcCARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwuY3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsFAAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJouM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwuX4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlGPfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6KOqkqm57TH2H3eDJAkSnh6/DNFu0QgADTjCCA0owggIyoAMCAQICEESvsIDWoye6iTA5hi74QGswDQYJKoZIhvcNAQEFBQAwPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzAeFw0wMDA5MzAyMTEyMTlaFw0yMTA5MzAxNDAxMTVaMD8xJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjEXMBUGA1UEAxMORFNUIFJvb3QgQ0EgWDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDfr+mXUAiDV7TMYmX2kILsx9MsazDKW+zZw33HQMEYFIvg6DN2SSrjPyFJk6xODq8+SMtl7vzTIQ9l0irZMo+M5fd3sBJ7tZXAiaOpuu1zLnoMBjKDon6KFDDNEaDhKji5eQox/VC9gGXft1Fjg8jiiGHqS2GB7FJruaLiSxoon0ijngzaCY4+Fy4e3SDfW8YqiqsuvXCtxQsaJZB0csV7aqs01jCJ/+VoE3tUC8jWruxanJIePWSzjMbfv8lBcOwWctUm7DhVOUPQ/P0YXEDxl+vVmpuNHbraJbnG2N/BFQI6q9pu8T4u9VwInDzWg2nkEJsZKrYpV+PlPZuf8AJdAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTEp7Gkeyxx+tvhS5B1/8QVYIWJEDANBgkqhkiG9w0BAQUFAAOCAQEAoxosmxcAXKke7ihmNzq/g8c/S8MJoJUgXePZWUTSPg0+vYpLoHQfzhCCnHQaHX6YGt3LE0uzIETkkenM/H2l22rl/ub94E7dtwA6tXBJr/Ll6wLx0QKLGcuUOl5IxBgeWBlfHgJa8Azxsa2p3FmGi27pkfWGyvq5ZjOqWVvO4qcWc0fLK8yZsDdIz+NWS/XPDwxyMofG8ES7U3JtQ/UmSJpSZ7dYq/5ndnF42w2iVhQTOSQxhaKoAlowR+HdUAe8AgmQAOtkY2CbFryIyRLm0n2Ri/k9Mo1ltOl8sVd26sW2KDm/FWUcyPZ3lmoKjXcL2JELBI4H2ym2Cu6dgjU1EA==", 5 | "leaf_input" => "AAAAAAFht5M9XwAAAAT7MIIE9zCCA9+gAwIBAgISAzj6DGI7JHoS4cDJ2/1HUC7MMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xODAyMjEwNzU1MzVaFw0xODA1MjIwNzU1MzVaMBUxEzARBgNVBAMTCmRhbmFkanMuY2gwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDP5RtGzUprBBMseQX34dl5OGhkZJBk3y5JRlv3axHynhMFSSUBiChy6ujHOVzDEIDj4EcidgR4QB9MksObR6NETwxCmqt/d950G0CLBBlyD2/dAu1RdWRrqgE1H2OR6zkI/qGZNILnEYI1IFG1rpn0syoJUHS/AXgz3oDpY7YX4hO2d0lhqn8lR8zsWKZXZzmNMlP7VXE8GZvi7dpFnBLjCcCBNafiIOSHFzaNVaeY5kumfzENv5Ij+sNdErzvrTFtlji5R28/uguyYlC3rZ0Lx6ZCRyL789cLP3cVjoK2vIW6aF6i4UdywLaFiusV5qsjLCfjs1/q7d9JDUJIiNlxAgMBAAGjggIKMIICBjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNZ70J9N1iGFQFOaT/AXJsqFZHvaMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9yZy8wFQYDVR0RBA4wDIIKZGFuYWRqcy5jaDCB/gYDVR0gBIH2MIHzMAgGBmeBDAECATCB5gYLKwYBBAGC3xMBAQEwgdYwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIGrBggrBgEFBQcCAjCBngyBm1RoaXMgQ2VydGlmaWNhdGUgbWF5IG9ubHkgYmUgcmVsaWVkIHVwb24gYnkgUmVseWluZyBQYXJ0aWVzIGFuZCBvbmx5IGluIGFjY29yZGFuY2Ugd2l0aCB0aGUgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vbGV0c2VuY3J5cHQub3JnL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUAA4IBAQCF4QtYBj3r0yMVAsok2E1/vu+1cNgJACFMmQPo0COfG7iOFVDXdOc/mvcEAzjW5YG8TiaKaNjaLukWzCVWWNOMECzP45AQ1gc2Hy3Amu5aJkSwLV6oc6mdZB9Mwo91JKYmA5JHaCsObS1aOPjbOkNCCR9Qg6CL/W+8c4kLjMw6Z5DbmUkLpznfQ5oXClIy3g+AQKbxFmlSpXNDjbPlNSgCQm/wDZJf3EWPpmd/ztybPRo/cY3Y8bX0HHgLAwNr6zYoCO0aiWVchdMwLa520ETH0vCoAj5/uo+4XCuyq2eabYI0rVAsBAdGyd3Yi5FAuPG5Q8ls0qGTpj6JmUlUShaLAAA=" 6 | } 7 | 8 | test "decodes properly" do 9 | parsed_entry = Certstream.CTParser.parse_entry(@standard_entry) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------