├── .gitignore ├── LICENSE.txt ├── README.mkd ├── app.py ├── conf └── .gitkeep ├── modules ├── __init__.py ├── admin.py ├── irc.py ├── kwlinker │ ├── __init__.py │ ├── css.py │ ├── html.py │ ├── nginx.py │ └── php.py ├── paste.py ├── sanity.py └── utils.py ├── static ├── css │ ├── site.css │ └── site.css.orig └── images │ ├── favicon.ico │ └── logo.png └── views ├── about.html ├── base.html ├── error.html ├── page.html ├── paste.html ├── raw.html └── view.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | conf/settings.cfg 3 | 4 | # PyCharm IDE 5 | .idea/ 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Michael Lustfield 2 | Copyright (c) 2013, Ngx CC 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name of the Ngx CC nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.mkd: -------------------------------------------------------------------------------- 1 | # Ngxbot Pastebin Service 2 | 3 | This tiny pastebin service is written in python with [bottle](https://bottlepy.org/). 4 | 5 | What makes this one different? 6 | 7 | Well... It has integration with the #nginx IRC channel. It also doesn't store 8 | private data to disk. The paste is only active until the cache is cleared. this 9 | makes it ideal as a fast service for quick support. It also has extra Nginx 10 | hilighting features. 11 | 12 | Required Software 13 | ----------------- 14 | 15 | Python modules: 16 | 17 | - python3-cymruwhois 18 | - python3-bottle 19 | - python3-redis 20 | - python3-jinja2 21 | - python3-pygments 22 | 23 | Additional Software needed: [`redis-server`](http://redis.io/download) 24 | 25 | Required Files 26 | -------------- 27 | 28 | conf/settings.cfg:: 29 | 30 | [bottle] 31 | cache_host=localhost 32 | cache_db=0 33 | port=80 34 | root_path=. 35 | url=http://p.ngx.cc/ 36 | relay_enabled=False 37 | relay_chan=sectionName 38 | relay_host=server.domain.tld 39 | relay_port=4040 40 | python_server=auto 41 | 42 | Credits 43 | ------- 44 | 45 | Originally written by Michael Lustfield (MTecknology). 46 | 47 | Theme concepts taken from readthedocs.org and dpaste.com. 48 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # local imports 4 | import modules.admin as admin 5 | import modules.irc as irc 6 | import modules.paste as paste 7 | import modules.sanity as sanity 8 | import modules.utils as utils 9 | 10 | import bottle 11 | import configparser 12 | import redis 13 | 14 | # Load Settings 15 | conf = configparser.ConfigParser({ 16 | 'cache_host': 'localhost', 17 | 'cache_db': 0, 18 | 'cache_ttl': 360, 19 | 'port': 80, 20 | 'root_path': '.', 21 | 'url': '', 22 | 'relay_enabled': True, 23 | 'relay_chan': '', 24 | 'relay_port': 5050, 25 | 'relay_pass': 'nil', 26 | 'recaptcha_sitekey': '', 27 | 'recaptcha_secret': '', 28 | 'check_spam': False, 29 | 'admin_key': '', 30 | 'python_server': 'auto'}) 31 | conf.read('conf/settings.cfg') 32 | 33 | app = application = bottle.Bottle() 34 | cache = redis.StrictRedis(host=conf.get('bottle', 'cache_host'), db=int(conf.get('bottle', 'cache_db'))) 35 | 36 | 37 | @app.route('/static/') 38 | def static(filename): 39 | ''' 40 | Serve static files 41 | ''' 42 | return bottle.static_file(filename, root='{}/static'.format(conf.get('bottle', 'root_path'))) 43 | 44 | 45 | @app.error(500) 46 | @app.error(404) 47 | def errors(code): 48 | ''' 49 | Handler for errors 50 | ''' 51 | return bottle.jinja2_template('error.html', code=code) 52 | 53 | 54 | @app.route('/') 55 | def new_paste(): 56 | ''' 57 | Display page for new empty post 58 | ''' 59 | (data, template) = paste.new_paste(conf) 60 | return bottle.jinja2_template('paste.html', data=data, tmpl=template) 61 | 62 | 63 | @app.route('/f/') 64 | def new_fork(paste_id): 65 | ''' 66 | Display page for new fork from a paste 67 | ''' 68 | (data, template) = paste.new_paste(conf, cache, paste_id) 69 | return bottle.jinja2_template('paste.html', data=data, tmpl=template) 70 | 71 | 72 | @app.route('/', method='POST') 73 | def submit_paste(): 74 | ''' 75 | Put a new paste into the database 76 | ''' 77 | return paste.submit_new(conf, cache) 78 | 79 | 80 | @app.route('/') 81 | def view_paste(paste_id): 82 | ''' 83 | Return page with . 84 | ''' 85 | data = paste.get_paste(cache, paste_id) 86 | return bottle.jinja2_template('view.html', paste=data, pid=paste_id) 87 | 88 | 89 | @app.route('/r/') 90 | def view_raw(paste_id): 91 | ''' 92 | View raw paste with . 93 | ''' 94 | data = paste.get_paste(cache, paste_id, raw=True) 95 | bottle.response.add_header('Content-Type', 'text/plain; charset=utf-8') 96 | return data['code'] 97 | 98 | 99 | @app.route('/d//') 100 | def view_diff(orig, fork): 101 | ''' 102 | View the diff between a paste and what it was forked from 103 | ''' 104 | if not cache.exists('paste:' + orig) or not cache.exists('paste:' + fork): 105 | return bottle.jinja2_template('error.html', code=200, 106 | message='At least one paste could not be found.') 107 | 108 | diff = paste.gen_diff(cache, orig, fork) 109 | return bottle.jinja2_template('page.html', data=diff) 110 | 111 | 112 | @app.post('/admin') 113 | def exec_admin(): 114 | return admin.run_cmd(conf, cache) 115 | 116 | 117 | @app.route('/about') 118 | def show_info(): 119 | return bottle.jinja2_template('about.html') 120 | 121 | 122 | if __name__ == '__main__': 123 | app.run( 124 | host='0.0.0.0', 125 | port=conf.getint('bottle', 'port')) 126 | -------------------------------------------------------------------------------- /conf/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxirc/pbin/f3b06d132c9be8e36e72750cfe28f2be4a99d638/conf/.gitkeep -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxirc/pbin/f3b06d132c9be8e36e72750cfe28f2be4a99d638/modules/__init__.py -------------------------------------------------------------------------------- /modules/admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # local imports 4 | from . import sanity 5 | 6 | import bottle 7 | import json 8 | 9 | 10 | def run_cmd(conf, cache): 11 | ''' 12 | Execute an administrative command. 13 | ''' 14 | data = bottle.request.json 15 | admin_key = conf.get('bottle', 'admin_key') 16 | 17 | # Supported commands 18 | commands = { 19 | 'blacklist_paste': _cmd_blacklist_paste, 20 | 'bl': _cmd_blacklist_paste, 21 | 'delete_paste': _cmd_delete_paste, 22 | 'del': _cmd_delete_paste, 23 | 'whitelist_address': _cmd_whitelist_address, 24 | 'wl': _cmd_whitelist_address, 25 | 'greylist_address': _cmd_greylist_address, 26 | 'gl': _cmd_greylist_address} 27 | 28 | # Pre-flight checks 29 | err = None 30 | if not admin_key: 31 | err = 'No admin key configured.' 32 | elif not data: 33 | err = 'No payload decoded.' 34 | elif 'token' not in data: 35 | err = 'No auth provided.' 36 | elif data['token'] != admin_key: 37 | err = 'Invalid auth.' 38 | elif 'command' not in data: 39 | err = 'No command provided.' 40 | elif data['command'] not in commands: 41 | err = 'Command not supported.' 42 | if err: 43 | return {'message': err, 'status': 'error'} 44 | 45 | # Flight 46 | bottle.response.headers['Content-Type'] = 'application/json' 47 | resp = commands[data['command']](cache, data) 48 | return json.dumps(resp) 49 | 50 | 51 | def _cmd_blacklist_paste(cache, data): 52 | # Delete paste 53 | _ = _delete_paste(cache, data) 54 | 55 | # Block address 56 | addr = bottle.request.environ.get('REMOTE_ADDR', 'undef').strip() 57 | if sanity.blacklist_address(cache, addr): 58 | return {'message': 'Added to black list; paste removed.'.format(addr), 'status': 'success'} 59 | return {'message': 'unexpected error; task already complete?', 'status': 'error'} 60 | 61 | 62 | def _cmd_delete_paste(cache, data): 63 | ret = _delete_paste(cache, data) 64 | if ret: 65 | return ret 66 | return {'message': 'Paste deleted.', 'status': 'success'} 67 | 68 | 69 | def _delete_paste(cache, data): 70 | paste = data.get('target') 71 | if not paste: 72 | return {'message': 'No paste provided.', 'status': 'error'} 73 | paste_id = 'paste:{}'.format(data['target']) 74 | 75 | # Find paste origin address 76 | paste = cache.get(paste_id) 77 | if not paste: 78 | return {'message': 'Paste not found.', 'status': 'error'} 79 | addr = json.loads(paste)['origin_addr'] 80 | 81 | # Remove paste 82 | cache.delete(paste_id) 83 | 84 | 85 | def _cmd_whitelist_address(cache, data): 86 | addr = data.get('target') 87 | if not addr: 88 | return {'message': 'No address provided.', 'status': 'error'} 89 | 90 | if sanity.whitelist_address(cache, addr): 91 | return {'message': 'Removed from filtering.', 'status': 'success'} 92 | return {'message': 'unexpected error; task already complete?', 'status': 'error'} 93 | 94 | 95 | def _cmd_greylist_address(cache, data): 96 | ''' 97 | Don't block an address from using the service, but disable IRC relay. 98 | ''' 99 | paste = data.get('target') 100 | if not paste: 101 | return {'message': 'No paste provided.', 'status': 'error'} 102 | paste_id = 'paste:{}'.format(paste) 103 | 104 | # Find paste origin address 105 | cpaste = cache.get(paste_id) 106 | if not cpaste: 107 | return {'message': 'Paste not found.', 'status': 'error'} 108 | addr = json.loads(cpaste)['origin_addr'] 109 | 110 | # Greylist address 111 | if sanity.greylist_address(cache, addr): 112 | return {'message': 'Added to grey list.', 'status': 'success'} 113 | return {'message': 'unexpected error; task already complete?', 'status': 'error'} 114 | -------------------------------------------------------------------------------- /modules/irc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # local imports 4 | from . import utils 5 | 6 | import json 7 | import socket 8 | 9 | 10 | def send_message(conf, cache, paste, paste_id): 11 | ''' 12 | Send notification to channels 13 | ''' 14 | host = conf.get('bottle', 'relay_host') 15 | port = int(conf.get('bottle', 'relay_port')) 16 | pw = conf.get('bottle', 'relay_pass') 17 | 18 | # Build the message to send to the channel 19 | if paste['forked_from']: 20 | orig = json.loads(cache.get('paste:' + paste['forked_from'])) 21 | message = ''.join(['Paste from ', orig['name'], 22 | ' forked by ', paste['name'], ': [ ', 23 | conf.get('bottle', 'url'), paste_id, ' ]']) 24 | else: 25 | message = ''.join(['Paste from ', paste['name'], ': [ ', 26 | conf.get('bottle', 'url'), paste_id, ' ]']) 27 | 28 | # Only relay if paste is not private 29 | if conf.get('bottle', 'relay_chan') is not None and not utils.str2bool(paste['private']): 30 | # For each channel, send the relay server a message 31 | # Note: Irccat uses section names, not channels 32 | for section in conf.get('bottle', 'relay_chan').split(','): 33 | try: 34 | s = socket.create_connection((host, port)) 35 | s.send('{};{};{}\n'.format(section, pw, message).encode()) 36 | s.close() 37 | except: 38 | print('Unable to send message to {}'.format(section)) 39 | -------------------------------------------------------------------------------- /modules/kwlinker/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from . import html 4 | from . import nginx 5 | from . import php 6 | from . import css 7 | 8 | from pygments.filter import simplefilter 9 | from pygments.token import Token 10 | 11 | @simplefilter 12 | def logging_linker(self, lexer, stream, options): 13 | for ttype, value in stream: 14 | print("value: '{}' ttype: '{}'".format(value, ttype)) 15 | yield ttype, value 16 | 17 | def get_linker_by_name(name): 18 | if 'nginx' == name: 19 | return nginx.linker() 20 | elif 'html' == name: 21 | return html.linker() 22 | elif 'php' == name: 23 | return php.linker() 24 | elif 'css' == name: 25 | return css.linker() 26 | #else: 27 | # return logging_linker() 28 | return None 29 | 30 | _lnk_replacements = { 31 | '_LNK_B_': '', 33 | '_LNK_E_': '', 34 | '_LNK__': '_LNK_', 35 | } 36 | 37 | _lnk_regex = re.compile('|'.join(_lnk_replacements.keys())) 38 | 39 | def _lnk_replace(match): 40 | return _lnk_replacements[match.group()] 41 | 42 | def replace_markup(markup): 43 | return _lnk_regex.sub(_lnk_replace, markup) 44 | -------------------------------------------------------------------------------- /modules/kwlinker/css.py: -------------------------------------------------------------------------------- 1 | from pygments.filter import simplefilter 2 | from pygments.token import Token 3 | 4 | @simplefilter 5 | def linker(self, lexer, stream, options): 6 | for ttype, value in stream: 7 | value = value.replace('_LNK_', '_LNK__') 8 | if ttype is Token.Name.Tag and '<' == value[0]: 9 | for i in range(len(value)): 10 | if value[i].isalpha(): break 11 | if value[-1].isalpha(): 12 | j = len(value) + 1 13 | else: 14 | for j in range(i + 1, len(value)): 15 | if not value[j].isalpha(): break 16 | value = '%s_LNK_B_reference.sitepoint.com/css/%s_LNK_M_%s_LNK_E_%s' % (value[:i], value[i:j], value[i:j], value[j:]) 17 | yield ttype, value 18 | -------------------------------------------------------------------------------- /modules/kwlinker/html.py: -------------------------------------------------------------------------------- 1 | from pygments.filter import simplefilter 2 | from pygments.token import Token 3 | 4 | @simplefilter 5 | def linker(self, lexer, stream, options): 6 | for ttype, value in stream: 7 | value = value.replace('_LNK_', '_LNK__') 8 | if ttype is Token.Name.Tag and '<' == value[0]: 9 | for i in range(len(value)): 10 | if value[i].isalpha(): break 11 | if value[-1].isalpha(): 12 | j = len(value) + 1 13 | else: 14 | for j in range(i + 1, len(value)): 15 | if not value[j].isalpha(): break 16 | value = '%s_LNK_B_www.w3schools.com/tags/tag_%s.asp_LNK_M_%s_LNK_E_%s' % (value[:i], value[i:j], value[i:j], value[j:]) 17 | yield ttype, value 18 | -------------------------------------------------------------------------------- /modules/kwlinker/nginx.py: -------------------------------------------------------------------------------- 1 | from pygments.filter import simplefilter 2 | from pygments.token import Token 3 | 4 | @simplefilter 5 | def linker(self, lexer, stream, options): 6 | for ttype, value in stream: 7 | value = value.replace('_LNK_', '_LNK__') 8 | if ttype is Token.Keyword or ttype is Token.Keyword.Namespace: 9 | value = '_LNK_B_nginx.org/r/%s_LNK_M_%s_LNK_E_' % (value, value) 10 | yield ttype, value 11 | -------------------------------------------------------------------------------- /modules/kwlinker/php.py: -------------------------------------------------------------------------------- 1 | from pygments.filter import simplefilter 2 | from pygments.token import Token 3 | 4 | @simplefilter 5 | def linker(self, lexer, stream, options): 6 | for ttype, value in stream: 7 | value = value.replace('_LNK_', '_LNK__') 8 | if ttype is Token.Name.Builtin: 9 | value = '_LNK_B_www.php.net/manual/en/function.%s.php_LNK_M_%s_LNK_E_' % (value.replace('_', '-'), value) 10 | yield ttype, value 11 | -------------------------------------------------------------------------------- /modules/paste.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # local imports 4 | from . import irc 5 | from . import kwlinker 6 | from . import sanity 7 | from . import utils 8 | 9 | from pygments import highlight 10 | from pygments.lexers import * 11 | from pygments.formatters import HtmlFormatter 12 | 13 | import binascii 14 | import bottle 15 | import difflib 16 | import json 17 | import os 18 | 19 | 20 | class HtmlLineFormatter(HtmlFormatter): 21 | ''' 22 | Output as html and wrap each line in a span 23 | ''' 24 | name = 'Html with line wrap' 25 | aliases = ['htmlline'] 26 | 27 | def wrap(self, source, outfile): 28 | return self._wrap_div(self._wrap_pre(self._wrap_lines(source))) 29 | 30 | def _wrap_lines(self, source): 31 | i = self.linenostart 32 | for t, line in source: 33 | if t == 1: 34 | line = '%s' % (i, line) 35 | i += 1 36 | yield t, line 37 | 38 | 39 | def new_paste(conf, cache=None, paste_id=None): 40 | ''' 41 | Returns templating data for a new post page. 42 | ''' 43 | if conf.get('bottle', 'recaptcha_sitekey') and conf.get('bottle', 'check_spam'): 44 | template = {'recap_enabled': True, 'sitekey': conf.get('bottle', 'recaptcha_sitekey')} 45 | else: 46 | template = {'recap_enabled': False} 47 | 48 | # Default page values 49 | data = { 50 | 'name': '', 51 | 'syntax': 'nginx', 52 | 'private': '0'} 53 | 54 | if paste_id and cache: 55 | paste = json.loads(cache.get('paste:' + paste_id)) 56 | if paste: 57 | data.update(paste) 58 | data['paste_id'] = paste_id 59 | data['private'] = str(utils.str2int(paste['private'])) 60 | data['name'] = '' 61 | 62 | cookie = bottle.request.cookies.get('dat', None) 63 | if cookie: 64 | data.update(json.loads(cookie)) 65 | data['private'] = str(utils.str2int(data['private'])) 66 | 67 | return (data, template) 68 | 69 | 70 | def get_paste(cache, paste_id, raw=False): 71 | ''' 72 | Return page with . 73 | ''' 74 | paste_id = paste_id 75 | if not cache.exists('paste:' + paste_id): 76 | bottle.redirect('/') 77 | 78 | data = json.loads(cache.get('paste:' + paste_id)) 79 | 80 | if not raw: 81 | # Syntax hilighting 82 | try: 83 | lexer = get_lexer_by_name(data['syntax'], stripall=False) 84 | except: 85 | lexer = get_lexer_by_name('text', stripall=False) 86 | formatter = HtmlLineFormatter(linenos=True, cssclass="paste") 87 | linker = kwlinker.get_linker_by_name(data['syntax']) 88 | if linker is not None: 89 | lexer.add_filter(linker) 90 | data['code'] = highlight(data['code'], lexer, formatter) 91 | data['code'] = kwlinker.replace_markup(data['code']) 92 | else: 93 | data['code'] = highlight(data['code'], lexer, formatter) 94 | data['css'] = HtmlLineFormatter().get_style_defs('.code') 95 | 96 | return data 97 | 98 | 99 | def gen_diff(cache, orig, fork): 100 | ''' 101 | Returns a generated diff between two pastes. 102 | ''' 103 | po = json.loads(cache.get('paste:' + orig)) 104 | pf = json.loads(cache.get('paste:' + fork)) 105 | co = po['code'].split('\n') 106 | cf = pf['code'].split('\n') 107 | lo = '' + orig + '' 108 | lf = '' + fork + '' 109 | 110 | return difflib.HtmlDiff().make_table(co, cf, lo, lf) 111 | 112 | 113 | def submit_new(conf, cache): 114 | ''' 115 | Handle processing for a new paste. 116 | ''' 117 | paste_data = { 118 | 'code': bottle.request.POST.get('code', ''), 119 | 'name': bottle.request.POST.get('name', '').strip(), 120 | 'phone': bottle.request.POST.get('phone', '').strip(), 121 | 'private': bottle.request.POST.get('private', '0').strip(), 122 | 'syntax': bottle.request.POST.get('syntax', '').strip(), 123 | 'forked_from': bottle.request.POST.get('forked_from', '').strip(), 124 | 'webform': bottle.request.POST.get('webform', '').strip(), 125 | 'origin_addr': bottle.request.environ.get('REMOTE_ADDR', 'undef').strip(), 126 | 'recaptcha_answer': bottle.request.POST.get('g-recaptcha-response', '').strip()} 127 | cli_post = True if paste_data['webform'] == '' else False 128 | 129 | # Handle file uploads 130 | if type(paste_data['code']) == bottle.FileUpload: 131 | paste_data['code'] = '# FileUpload: {}\n{}'.format( 132 | paste_data['code'].filename, 133 | paste_data['code'].file.getvalue()) 134 | 135 | # Validate data 136 | (valid, err) = sanity.validate_data(conf, paste_data) 137 | if not valid: 138 | return bottle.jinja2_template('error.html', code=200, message=err) 139 | 140 | # Check recapcha answer if not cli post 141 | if utils.str2bool(conf.get('bottle', 'check_spam')) and not cli_post: 142 | if not sanity.check_captcha( 143 | conf.get('bottle', 'recaptcha_secret'), 144 | paste_data['recaptcha_answer']): 145 | return bottle.jinja2_template('error.html', code=200, message='Invalid captcha verification. ERR:677') 146 | 147 | # Check address against blacklist 148 | if sanity.address_blacklisted(cache, paste_data['origin_addr']): 149 | return bottle.jinja2_template('error.html', code=200, message='Address blacklisted. ERR:840') 150 | 151 | # Stick paste into cache 152 | paste_id = _write_paste(cache, paste_data) 153 | 154 | # Set cookie for user 155 | bottle.response.set_cookie( 156 | 'dat', 157 | json.dumps({ 158 | 'name': str(paste_data['name']), 159 | 'syntax': str(paste_data['syntax']), 160 | 'private': str(paste_data['private'])})) 161 | 162 | # Send user to page, or a link to page 163 | if cli_post: 164 | scheme = bottle.request.environ.get('REQUEST_SCHEME') 165 | host = bottle.request.get_header('host') 166 | return '{}://{}/{}\n'.format(scheme, host, paste_id) 167 | else: 168 | # Relay message to IRC 169 | if utils.str2bool(conf.get('bottle', 'relay_enabled')) and not sanity.address_greylisted(cache, paste_data['origin_addr']) and utils.str2bool(paste_data['private']): 170 | irc.send_message(conf, cache, paste_data, paste_id) 171 | bottle.redirect('/' + paste_id) 172 | 173 | 174 | def _write_paste(cache, paste_data): 175 | ''' 176 | Put a new paste into cache. 177 | Returns paste_id. 178 | ''' 179 | # Public pastes should have an easy to type key 180 | # Private pastes should have a more secure key 181 | id_length = 1 182 | if paste_data['recaptcha_answer'] == '': 183 | id_length = 12 184 | elif utils.str2bool(paste_data['private']): 185 | id_length = 8 186 | 187 | # Pick a unique ID 188 | paste_id = binascii.b2a_hex(os.urandom(id_length)).decode('utf-8') 189 | 190 | # Make sure it's actually unique or else create a new one and test again 191 | while cache.exists('paste:' + paste_id): 192 | id_length += 1 193 | paste_id = binascii.b2a_hex(os.urandom(id_length)).decode('utf-8') 194 | 195 | # Put the paste into cache 196 | cache.setex('paste:' + paste_id, 345600, json.dumps(paste_data)) 197 | 198 | return paste_id 199 | -------------------------------------------------------------------------------- /modules/sanity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # local imports 4 | from . import utils 5 | 6 | import cymruwhois 7 | import bottle 8 | import requests 9 | import re 10 | 11 | 12 | def validate_data(conf, paste_data): 13 | ''' 14 | Basic data validation. 15 | Returns (valid, error_message). 16 | ''' 17 | codetype = type(paste_data['code']) 18 | error = None 19 | 20 | if max(0, bottle.request.content_length) > bottle.request.MEMFILE_MAX: 21 | error = 'This request is too large to process. ERR:991' 22 | 23 | elif codetype != str and codetype != bottle.FileUpload: 24 | error = 'Invalid code type submitted. ERR:280' 25 | 26 | elif not re.match(r'^[a-zA-Z\[\]\\{}|`\-_][a-zA-Z0-9\[\]\\{}|`\-_]*$', paste_data['name']): 27 | error = 'Invalid input detected. ERR:925' 28 | 29 | elif paste_data['phone'] != '': 30 | error = 'Your post triggered our spam filters! ERR:228' 31 | 32 | for k in ['code', 'private', 'syntax', 'name']: 33 | if paste_data[k] == '': 34 | error = 'All fields need to be filled out. ERR:577' 35 | 36 | if error: 37 | return (False, error) 38 | return (True, None) 39 | 40 | 41 | def check_captcha(secret, answer, addr=None): 42 | ''' 43 | Returns True if captcha response is valid. 44 | ''' 45 | provider = 'https://www.google.com/recaptcha/api/siteverify' 46 | qs = { 47 | 'secret': secret, 48 | 'response': answer} 49 | if addr: 50 | qs['remoteip'] = addr 51 | 52 | response = requests.get(provider, params=qs) 53 | result = response.json() 54 | 55 | return result['success'] 56 | 57 | 58 | def address_blacklisted(cache, addr): 59 | ''' 60 | Returns True if address is currently blacklisted. 61 | ''' 62 | subnet = _addr_subnet(addr) 63 | if not subnet: 64 | # Fail open? 65 | return None 66 | if cache.exists('ipblock:{}'.format(utils.sha512(subnet))): 67 | return True 68 | return False 69 | 70 | 71 | def blacklist_address(cache, addr): 72 | ''' 73 | Add address to blacklist. 74 | Returns True if successfully added or False if error encountered. 75 | ''' 76 | subnet = _addr_subnet(addr) 77 | if not subnet: 78 | return False 79 | cache.setex('ipblock:{}'.format(utils.sha512(subnet)), 345600, 'nil') 80 | return True 81 | 82 | 83 | def whitelist_address(cache, addr): 84 | ''' 85 | Remove address from blacklist. 86 | Returns True if successfully removed or False if error encountered. 87 | ''' 88 | subnet = _addr_subnet(addr) 89 | if not subnet: 90 | return False 91 | cache.delete('ipblock:{}'.format(utils.sha512(subnet))) 92 | cache.delete('ipgrey:{}'.format(utils.sha512(subnet))) 93 | return True 94 | 95 | 96 | def address_greylisted(cache, addr): 97 | ''' 98 | Returns True if address is currently greylisted. 99 | ''' 100 | subnet = _addr_subnet(addr) 101 | if not subnet: 102 | # Fail open? 103 | return None 104 | if cache.exists('ipgrey:{}'.format(utils.sha512(subnet))): 105 | return True 106 | return False 107 | 108 | 109 | def greylist_address(cache, addr): 110 | ''' 111 | Add an address to grey listing: don't block, don't relay. 112 | Returns True if successfully added or False if error encountered. 113 | ''' 114 | subnet = _addr_subnet(addr) 115 | if not subnet: 116 | return False 117 | cache.setex('ipgrey:{}'.format(utils.sha512(subnet)), 345600, 'nil') 118 | return True 119 | 120 | 121 | def _addr_subnet(addr): 122 | ''' 123 | Returns a subnet for an address. 124 | ''' 125 | client = cymruwhois.Client() 126 | try: 127 | resp = client.lookup(addr) 128 | return resp.prefix 129 | except: 130 | return None 131 | -------------------------------------------------------------------------------- /modules/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import hashlib 4 | 5 | 6 | def str2bool(v): 7 | ''' 8 | Convert string to boolean. 9 | ''' 10 | return v.lower() in ('yes', 'true', 't', 'y', '1', 'on') 11 | 12 | 13 | def str2int(v): 14 | ''' 15 | Convert string to boolean to integer. 16 | ''' 17 | return int(str2bool(v)) 18 | 19 | 20 | def sha512(v): 21 | ''' 22 | Returns a sha512 checksum for provided value. 23 | ''' 24 | return hashlib.sha512(v.encode('utf-8')).hexdigest() 25 | -------------------------------------------------------------------------------- /static/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #eee; 3 | font-family: arial,helvetica,sans-serif; 4 | } 5 | 6 | #title, #title img { 7 | height: 56px; 8 | } 9 | 10 | #contentColumn { 11 | margin-left: auto; 12 | margin-right: auto; 13 | margin-bottom: 20px; 14 | margin-top: -10px; 15 | width: 95%; 16 | font-family: Verdana,arial,helvetica,sans-serif; 17 | -moz-box-shadow: 0px 1px 5px #bbbbbb; 18 | -webkit-box-shadow: 0px 1px 5px #bbbbbb; 19 | background: #fff; 20 | -moz-border-radius: 0px 0px 10px 10px; 21 | -border-radius: 0px 0px 10px 10px; 22 | -webkit-border-radius: 0px 0px 10px 10px; 23 | } 24 | 25 | .pastetable { 26 | background-color: #dddddd; 27 | } 28 | 29 | #pasteform table { 30 | margin: 0px auto; 31 | border-left: 1px solid #f5f5f; 32 | } 33 | 34 | .inside { 35 | padding: 0 1.5em; 36 | } 37 | 38 | #contentColumn { 39 | padding-top: 3em; 40 | margin-top: 2em; 41 | } 42 | 43 | #contentColumn .inside { 44 | position: relative; 45 | top: -2em; 46 | padding: 0 0.5em; 47 | } 48 | 49 | th,td { 50 | padding: 0px 7px 0px 7px; 51 | color: #888888; 52 | } 53 | 54 | label,.pturl { 55 | color: #039639 !important; 56 | } 57 | 58 | h1 { 59 | font-size: 100%; 60 | text-align: center; 61 | color: #dd4814; 62 | font-family: Verdana,arial,helvetica,sans-serif; 63 | font-size: medium; 64 | font-weight: 400; 65 | padding-top: 0.4em; 66 | margin: 0; 67 | } 68 | 69 | textarea { 70 | font: 110% "Courier New",Courier,monospace; 71 | color: #000; 72 | width: 100%; 73 | } 74 | 75 | .linenos { 76 | font-size: medium; 77 | } 78 | 79 | div.code { 80 | font-size: small; 81 | border: 1px solid #dddddd; 82 | background-color: #eeeeee; 83 | -moz-border-radius: 4px; 84 | -webkit-border-radius: 4px; 85 | -border-radius: 4px; 86 | padding: 10px 0px; 87 | margin: 20px 10px 100px 10px; 88 | color: #000000; 89 | } 90 | 91 | div.code div { 92 | border: 0; 93 | margin: 0px 0px; 94 | !important; 95 | padding: 0px; 96 | -moz-box-shadow: 0px 1px 5px #eee; 97 | -webkit-box-shadow: 0px 1px 5px #eeeeee; 98 | -box-shadow: 0px 1px 5px #eeeeee; 99 | font-size: medium; 100 | } 101 | 102 | div.code a { 103 | color: #000000; 104 | text-decoration: none; 105 | font-weight: 700; 106 | font-size: 0.9em; 107 | padding: 1em; 108 | } 109 | 110 | .linecount:hover { 111 | background: #9999aa; 112 | display: block; 113 | } 114 | 115 | #phone { 116 | visibility: hidden; 117 | } 118 | 119 | .note { 120 | background: none repeat scroll 0 0 #9999aa; 121 | color: #f7f7f7; 122 | padding: 12px; 123 | margin-top: 50px; 124 | } 125 | 126 | table.diff { 127 | font-family: Courier; 128 | border: medium; 129 | } 130 | 131 | .diff a { 132 | color: #008000; 133 | } 134 | 135 | .diff_header { 136 | background-color: #e0e0e0; 137 | } 138 | 139 | td.diff_header { 140 | text-align: right; 141 | } 142 | 143 | .diff_next { 144 | background-color: #c0c0c0; 145 | } 146 | 147 | .diff_add { 148 | background-color: #aaffaa; 149 | } 150 | 151 | .diff_chg { 152 | background-color: #ffff77; 153 | } 154 | 155 | .diff_sub { 156 | background-color: #ffaaaa; 157 | } 158 | 159 | .bar div { 160 | display: inline-block; 161 | } 162 | -------------------------------------------------------------------------------- /static/css/site.css.orig: -------------------------------------------------------------------------------- 1 | /** 2 | * Layout 3 | */ 4 | 5 | body { 6 | background-color: #e8ecef; 7 | color: #000000; 8 | font: 100%/1.5 "ff-meta-web-pro-1","ff-meta-web-pro-2",Arial,"Helvetica Neue",sans-serif; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | #header { 14 | background-color: #465158; 15 | font-size: 0.8em; 16 | width: 100%; 17 | min-width: 800px; 18 | height: 3em; 19 | padding-top: 0.4em; 20 | } 21 | 22 | #header a { 23 | color: #ffffff; 24 | font-weight: bold; 25 | line-height: 2.4em; 26 | vertical-align: center; 27 | padding: 0.4em 0.8em; 28 | } 29 | 30 | #menu { 31 | float: right; 32 | list-style: none outside none; 33 | line-height: 2.5em; 34 | margin: 0; 35 | padding: 0 0 0 10px; 36 | } 37 | 38 | #menu a { 39 | background-color: #697983; 40 | border: medium none; 41 | border-radius: 3px 3px 3px 3px; 42 | margin: 0.375em 0; 43 | text-shadow: 0 1px rgba(0, 0, 0, 0.5); 44 | } 45 | 46 | #menu li { 47 | float: right; 48 | padding-right: 10px; 49 | } 50 | 51 | #content { 52 | padding: 20px; 53 | width: 700px; 54 | } 55 | 56 | .note { 57 | background: none repeat scroll 0 0 #9999aa; 58 | color: #f7f7f7; 59 | padding: 12px; 60 | margin-top: 50px; 61 | } 62 | 63 | /** 64 | * Content 65 | */ 66 | 67 | a { 68 | color: #ffffff; 69 | } 70 | 71 | .code a { 72 | color: #008000; 73 | } 74 | 75 | /** 76 | * Create Paste 77 | */ 78 | 79 | #pasteform td:first-child { 80 | text-align: right; 81 | } 82 | 83 | #phone { 84 | display: none; 85 | } 86 | 87 | /** 88 | * View Paste 89 | */ 90 | 91 | .bar { 92 | background: none repeat scroll 0 0 #465158; 93 | color: #fefefe; 94 | font-size: 75%; 95 | height: 18px; 96 | padding: 6px 15px; 97 | } 98 | 99 | .bar .title, 100 | .bar .syntax { 101 | float: left; 102 | } 103 | 104 | .bar .author, 105 | .bar .options { 106 | float: right; 107 | } 108 | 109 | .paste { 110 | background: none repeat scroll 0 0 #ffffff; 111 | padding: 0; 112 | } 113 | 114 | .paste .linenos { 115 | background: none repeat scroll 0 0 #eeeeee; 116 | color: #999999; 117 | padding: 0 5px; 118 | } 119 | 120 | .paste .code { 121 | padding: 0 8px; 122 | width: 100%; 123 | } 124 | 125 | /** 126 | * View Diff 127 | */ 128 | 129 | table.diff { 130 | font-family: Courier; 131 | border: medium; 132 | } 133 | 134 | .diff a { 135 | color: #008000; 136 | } 137 | 138 | .diff_header { 139 | background-color: #e0e0e0; 140 | } 141 | 142 | td.diff_header { 143 | text-align: right; 144 | } 145 | 146 | .diff_next { 147 | background-color: #c0c0c0; 148 | } 149 | 150 | .diff_add { 151 | background-color: #aaffaa; 152 | } 153 | 154 | .diff_chg { 155 | background-color: #ffff77; 156 | } 157 | 158 | .diff_sub { 159 | background-color: #ffaaaa; 160 | } 161 | -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxirc/pbin/f3b06d132c9be8e36e72750cfe28f2be4a99d638/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxirc/pbin/f3b06d132c9be8e36e72750cfe28f2be4a99d638/static/images/logo.png -------------------------------------------------------------------------------- /views/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block body %} 4 |

Overview

5 | This site is a tool that Ngx CC uses for helping users in the #nginx IRC 6 | channel on libera. It allows users to paste configuration files and other 7 | pieces of data to allow us to help them. 8 | 9 |

The Difference

10 | We created yet another pastebin service because we needed something that fit 11 | our needs better. We couldn't find something that we liked, so we started 12 | from scratch. 13 | 14 | This service: 15 |
    16 |
  • Integrates with the #nginx IRC channel
  • 17 |
  • Offers relevant syntax hilighting options
  • 18 |
  • Automatic expiration of data
  • 19 |
  • Has cookie based personal defaults
  • 20 |
  • Includes an IRC Nick option
  • 21 |
  • Blocks spam!
  • 22 |
  • More...
  • 23 |
24 | 25 |

Using Curl

26 | The best way to provide a complete picture of what's going on looks like this: 27 |
curl -X POST -F 'name=$your_nick' -F 'syntax=nginx' -F "code=$(nginx -T)" https://paste.nginx.org/ 28 |

Single config files can be uploaded: 29 |
curl -X POST -F 'name=$your_nick' -F 'syntax=nginx' -F 'code=@/etc/nginx/conf.d/mysite.conf' https://paste.nginx.org/ 30 |

It's also possible to provide logs this way: 31 |
curl -X POST -F 'name=$your_nick' -F 'syntax=nginx' -F "code=$(tail -n 100 /var/log/nginx/*.log)" https://paste.nginx.org/ 32 | 33 |

34 | If you have any questions or concerns about this, please check out the 35 | project page. 36 | 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /views/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% if title %} 9 | {% block title %}ngx pastebin{% endblock %} 10 | {% else %} 11 | ngx pastebin 12 | {% endif %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% block head %}{% endblock %} 20 | 21 | 22 | 23 |

24 |
25 |
26 | {% if title %}{{ title }}{% else %}ngx pastebin{% endif %} 27 |
28 | {% block body %}{% endblock %} 29 |
30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /views/error.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block body %} 4 | {% if code.status==404 %} 5 |

404, Page Not Found

6 |

You broke the world! How could you do this to me! :( 7 | It's ok. You just tried to access a page that doesn't exist.

8 | {% elif code.status==500 %} 9 |

500, Internal Server Error

10 |

It's not you, it's me. Something broke. Please yell at me.

11 | {% else %} 12 |

Error

13 |

{{ message }}

14 | {% endif %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /views/page.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block body %} 4 | {{ data }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /views/paste.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block body %} 4 |
5 | 6 | {% if 'paste_id' in data %}{% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {% if tmpl['recap_enabled'] %}{% endif %} 70 |
 
  46 | 47 |
48 | 69 |
71 |

72 | All fields are required. Your paste will be deleted after four days. 73 |
By default, the bot will post a link to your paste in the channel. If you are not posting this for use in the IRC channel, please check Private. 74 |
Click About to see more information. 75 |

76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /views/raw.html: -------------------------------------------------------------------------------- 1 | {{ code|e }} 2 | -------------------------------------------------------------------------------- /views/view.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head %} 4 | 5 | {% endblock %} 6 | 7 | {% block body %} 8 |
9 |
10 | Author: {{ paste['name']|e }} 11 |
12 |
13 |
14 | {{ paste['code'] }} 15 |
16 |
17 |
18 | Syntax: {{ paste['syntax']|e }} |  19 |
20 |
21 | Raw |  22 |
23 | {% if 'forked_from' in paste and paste['forked_from'] %} 24 |
25 | Diff |  26 |
27 | {% endif %} 28 |
29 | Fork 30 |
31 |
32 | {% endblock %} 33 | --------------------------------------------------------------------------------