Skip to content

Instantly share code, notes, and snippets.

@pdparchitect
Created December 14, 2018 13:35

Revisions

  1. pdparchitect created this gist Dec 14, 2018.
    15 changes: 15 additions & 0 deletions defaults.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    const os = require('os')
    const fs = require('fs')
    const path = require('path')

    const getDefaults = (name) => {
    const filepath = path.join(os.homedir(), '.pown', name + '.json')

    if (fs.existsSync(filepath)) {
    return JSON.parse(fs.readFileSync(filepath))
    } else {
    return {}
    }
    }

    exports.getDefaults = getDefaults
    151 changes: 151 additions & 0 deletions index.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,151 @@
    const debounce = require('debounce')
    const { screen, Question } = require('neo-blessed')

    const Table = require('./table')
    const Request = require('./request')
    const Response = require('./response')
    const { getDefaults } = require('./defaults')

    const { tui: tuiDefaults = {} } = getDefaults('proxy')

    const sc = screen({
    title: 'Proxy'
    })

    sc.key(['tab'], function (ch, key) {
    sc.focusNext()
    })

    sc.key(['q', 'C-c', 'C-x'], function (ch, key) {
    const question = new Question({
    keys: true,
    top: 'center',
    left: 'center',
    width: '50%',
    height: 5,
    border: {
    type: 'line'
    },
    style: {
    border: {
    fg: 'grey'
    }
    }
    })

    sc.append(question)

    question.ask('Do you really want to quit?', (err, result) => {
    if (err) {
    return
    }

    if (result) {
    return process.exit(0)
    }

    sc.remove(question)
    sc.render()
    })
    })

    const render = debounce(() => {
    sc.render()
    }, 1000)

    const transactions = new Table({
    ...tuiDefaults.transactions,

    top: 0,
    left: 0,
    width: '100%',
    height: '50%',
    border: {
    type: 'line'
    },
    style: {
    border: {
    fg: 'grey'
    }
    },
    columns: [
    { field: 'id', name: '#', width: 13 },
    { field: 'method', name: 'method', width: 7 },
    { field: 'scheme', name: 'scheme', width: 7 },
    { field: 'host', name: 'host', width: 13 },
    { field: 'port', name: 'port', width: 5 },
    { field: 'path', name: 'path', width: 42 },
    { field: 'query', name: 'query', width: 42 },
    { field: 'responseCode', name: 'code', width: 7 },
    { field: 'responseType', name: 'type', width: 13 },
    { field: 'responseLength', name: 'length', width: 21 }
    ],
    columnSpacing: 3
    })

    const request = new Request({
    ...tuiDefaults.request,

    bottom: 0,
    left: 0,
    width: '50%',
    height: '50%',
    border: {
    type: 'line'
    },
    style: {
    border: {
    fg: 'grey'
    }
    }
    })

    const response = new Response({
    ...tuiDefaults.response,

    bottom: 0,
    right: 0,
    width: '50%',
    height: '50%',
    border: {
    type: 'line'
    },
    style: {
    border: {
    fg: 'grey'
    }
    }
    })

    sc.append(transactions)
    sc.append(request)
    sc.append(response)

    transactions.on('select', (a) => {
    request.display(a)
    response.display(a)

    sc.render()
    })

    transactions.focus()

    let i = 0
    setInterval(() => {
    transactions.addItem({
    id: i++,
    method: 'GET',
    scheme: 'http',
    host: 'google.com',
    port: 80,
    path: '/' + Math.random(),
    query: '',
    responseCode: 200,
    responseType: 'html',
    responseLength: 1234
    })

    render()
    }, 1000)

    sc.render()
    55 changes: 55 additions & 0 deletions request.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,55 @@
    const { Box } = require('neo-blessed')

    const EMPTY = Buffer.from('')

    class Request extends Box {
    constructor (options) {
    options = {
    tags: true,
    scrollable: true,

    methodColors: {
    'GET': 'yellow',
    'POST': 'yellow',
    'HEAD': 'yellow',
    'PUT': 'purple',
    'PATCH': 'red',
    'DELETE': 'red'
    },

    ...options
    }

    super(options)
    }

    display (request) {
    const { method, scheme, host, port, path, query, version = 'HTTP/1.1', headers = {}, body = EMPTY } = request

    const methodColor = this.options.methodColors[method] || 'white'

    let addressBlock

    if ((scheme === 'http' && port === 80) || (scheme === 'https' && port === 443)) {
    addressBlock = `${host}`
    } else {
    addressBlock = `${host}:${port}`
    }

    const headersBlock = Object.entries(headers).map(([name, value]) => {
    if (!Array.isArray(value)) {
    value = [value]
    }

    return value.map((value) => {
    return `{purple-fg}${name}:{/pruple-fg} ${value}`
    }).join('\n')
    }).join('\n')

    const bodyBlock = body.toString()

    this.setContent(`{${methodColor}-fg}${method}{/${methodColor}-fg} ${scheme}://${addressBlock}${path}${query ? '?' + query : ''} ${version}\n${headersBlock}\n${bodyBlock}`)
    }
    }

    module.exports = Request
    44 changes: 44 additions & 0 deletions response.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,44 @@
    const { Box } = require('neo-blessed')

    const EMPTY = Buffer.from('')

    class Response extends Box {
    constructor (options) {
    options = {
    tags: true,
    scrollable: true,

    codeColors: {
    '1xx': 'cyan',
    '2xx': 'green',
    '3xx': 'yellow',
    '4xx': 'purple',
    '5xx': 'red'
    },

    ...options
    }
    super(options)
    }
    display (response) {
    const { responseCode, responseMessage, responseVersion = 'HTTP/1.1', responseHeaders = {}, responseBody = EMPTY } = response

    const codeColor = this.options.codeColors[responseCode.toString().replace(/(\d).*/, '$1xx')] || 'white'

    const headersBlock = Object.entries(responseHeaders).map(([name, value]) => {
    if (!Array.isArray(value)) {
    value = [value]
    }

    return value.map((value) => {
    return `{purple-fg}${name}:{/pruple-fg} ${value}`
    }).join('\n')
    }).join('\n')

    const bodyBlock = responseBody.toString()

    this.setContent(`{${codeColor}-fg}${responseCode}{/${codeColor}-fg} ${responseMessage} ${responseVersion}\n${headersBlock}\n${bodyBlock}`)
    }
    }

    module.exports = Response
    162 changes: 162 additions & 0 deletions table.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,162 @@
    const stripAnsi = require('strip-ansi')
    const { Box, List } = require('neo-blessed')

    class Table extends Box {
    constructor (options) {
    const { style = {} } = options
    const { columns: columnsStyle = {}, rows: rowsStyle = {} } = style
    const { selected: rowsSelectedStyle = {}, item: rowsItemStyle } = rowsStyle

    options = {
    columnSpacing: 10,
    columns: [],

    items: [],

    keys: true,
    vi: false,
    interactive: true,

    ...options,

    style: {
    ...style,

    columns: {
    bold: true,

    ...columnsStyle
    },

    rows: {
    selected: {
    fg: 'white',
    bg: 'blue',

    ...rowsSelectedStyle
    },

    item: {
    fg: 'white',
    bg: '',

    ...rowsItemStyle
    }
    }
    }
    }

    super(options)

    this.columns = new Box({
    screen: this.screen,
    parent: this,
    top: 0,
    left: 0,
    height: 1,
    width: 'shrink',
    ailgn: 'left',
    style: this.options.style.columns
    })

    this.rows = new List({
    screen: this.screen,
    parent: this,
    top: 2,
    left: 0,
    width: '100%',
    align: 'left',
    style: this.options.style.rows,
    keys: options.keys,
    vi: options.vi,
    interactive: options.interactive
    })

    this.append(this.columns)
    this.append(this.rows)

    this.on('attach', () => {
    this.setColumns(options.columns)
    this.setItems(options.items)
    })

    this.rows.on('select', (_, index) => {
    this.emit('select', this.items[index], index)
    })
    }

    focus () {
    this.rows.focus()
    }

    render () {
    if (this.screen.focused === this.rows) {
    this.rows.focus()
    }

    this.rows.width = this.width - 3
    this.rows.height = this.height - 4

    super.render()
    }

    fieldsToContent (fields) {
    let str = ''

    fields.forEach((field, index) => {
    const size = this.columnWidths[index]
    const strip = stripAnsi(field.toString())
    const len = field.toString().length - strip.length

    field = field.toString().substring(0, size + len)

    // compensate for len

    let spaceLength = size - strip.length + this.options.columnSpacing

    if (spaceLength < 0) {
    spaceLength = 0
    }

    const spaces = new Array(spaceLength).join(' ')

    str += field + spaces
    })

    return str
    }

    dataToContentItem (d) {
    return this.fieldsToContent(this.columnFields.map((f) => d[f]))
    }

    setColumns (columns) {
    this.columnFields = []
    this.columnNames = []
    this.columnWidths = []

    columns.forEach((column) => {
    const { field, name, width } = column

    this.columnFields.push(field)
    this.columnNames.push(name)
    this.columnWidths.push(width)
    })

    this.columns.setContent(this.fieldsToContent(this.columnNames))
    }

    setItems (items) {
    this.items = [...items]

    this.rows.setItems(items.map((item) => this.dataToContentItem(item)))
    }

    addItem (item) {
    this.items.push(item)

    this.rows.addItem(this.dataToContentItem(item))
    }
    }

    module.exports = Table