├── .nvmrc ├── .gitignore ├── binaries └── gist-proxy-server ├── sources ├── http.js ├── gist.js └── gist-proxy-server.js ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | iojs 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /binaries/gist-proxy-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require( 'babel/register' )( { only : [ require( 'path' ).resolve( __dirname + '/../sources' ) ] } ); 4 | require( '../sources/gist-proxy-server' ); 5 | -------------------------------------------------------------------------------- /sources/http.js: -------------------------------------------------------------------------------- 1 | export class Http extends Error { 2 | 3 | constructor( status, message ) { 4 | 5 | super( ); 6 | 7 | this.status = status; 8 | this.message = `HTTP ${status}: ${message}`; 9 | 10 | } 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gist-proxy-server", 3 | "description": "Make your gists editable by the internet if you want to", 4 | "version": "1.0.4", 5 | "bin": { 6 | "gist-proxy-server": "./binaries/gist-proxy-server" 7 | }, 8 | "dependencies": { 9 | "babel": "^5.8.23", 10 | "body-parser": "^1.13.3", 11 | "express": "^4.13.3", 12 | "minimist": "^1.2.0", 13 | "node-fetch": "^1.3.2", 14 | "yamljs": "^0.2.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sources/gist.js: -------------------------------------------------------------------------------- 1 | let fetch = require( 'node-fetch' ); 2 | 3 | let { Http } = require( './http' ); 4 | 5 | export let Gist = { 6 | 7 | get( user, password, uid ) { 8 | 9 | return fetch( `https://${user}:${password}@api.github.com/gists/${uid}` ).then( res => { 10 | 11 | if ( res.status === 404 ) 12 | throw new Http( 404, 'Gist not found' ); 13 | 14 | if ( ! res.ok ) 15 | throw new Http( 500, 'Unexpected error' ); 16 | 17 | return res.json( ); 18 | 19 | } ); 20 | 21 | }, 22 | 23 | edit( user, password, uid, file, body ) { 24 | 25 | return fetch( `https://${user}:${password}@api.github.com/gists/${uid}`, { method : 'PATCH', body : JSON.stringify( { files : { [file] : { content : body } } } ) } ).then( res => { 26 | 27 | if ( res.status === 404 ) 28 | throw new Http( 404, 'Gist not found' ); 29 | 30 | if ( ! res.ok ) 31 | throw new Http( 500, 'Unexpected error' ); 32 | 33 | return body; 34 | 35 | } ); 36 | 37 | } 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gist Proxy Server 2 | 3 | ## Command line 4 | 5 | ``` 6 | $> gist-proxy-server --username --password --port 7 | ``` 8 | 9 | You can also set your credentials using the `GIST_PROXY_SERVER_USERNAME` and `GIST_PROXY_SERVER_PASSWORD` environment variables. 10 | 11 | Note that your password may (and probably *should*, security-wise) be a Github access token, configured with the `gist` permission. 12 | 13 | ## Why? 14 | 15 | This utility let you open some of your gists to the internet. Via your proxy, anyone will be able to see them and modify them, according to the rules you will have set. 16 | 17 | I use it as a light database system for small webapps shared with few collaborators. 18 | 19 | ## Security 20 | 21 | In order to make a gist shareable through Gist Proxy Server, you first have to add a special file named `_gist-proxy-server.yml` in your gist. This file should contain a `files` property, which is a dictionnary where each key is a file name and each value is a permission (`all` / `readonly` / `writeonly`). 22 | 23 | Thanks to this permission model: 24 | 25 | - Your gists won't be shared unless you explicitely say so 26 | - The files from your gist won't be editable unless you explicitely say so 27 | 28 | ## Usage 29 | 30 | ### `GET /:uid/:file` 31 | 32 | Returns the content of the specified file, from the specified gist. 33 | 34 | ### `PUT /:uid/:file` 35 | 36 | Sets the content of the specified file, from the specified gist. 37 | 38 | ## License 39 | 40 | > **The MIT License (MIT)** 41 | > 42 | > Copyright © 2015 Maël Nison 43 | > 44 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 45 | > 46 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 47 | > 48 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 49 | -------------------------------------------------------------------------------- /sources/gist-proxy-server.js: -------------------------------------------------------------------------------- 1 | let bodyParser = require( 'body-parser' ); 2 | let express = require( 'express' ); 3 | let fetch = require( 'node-fetch' ); 4 | let minimist = require( 'minimist' ); 5 | let YAML = require( 'yamljs' ); 6 | 7 | let { Gist } = require( './gist' ); 8 | let { Http } = require( './http' ); 9 | 10 | let args = minimist( process.argv ); 11 | 12 | let user = args.username || process.env.GIST_PROXY_SERVER_USERNAME; 13 | let password = args.password || process.env.GIST_PROXY_SERVER_PASSWORD; 14 | let port = args.port || 80; 15 | 16 | function getGistConfig( uid ) { 17 | 18 | return Gist.get( user, password, uid ).then( function ( gist ) { 19 | 20 | if ( ! Object.hasOwnProperty.call( gist.files, '_gist-proxy-server.yml' ) ) 21 | throw new Http( 403, 'Missing configuration' ); 22 | 23 | let gistConfig = gist.files[ '_gist-proxy-server.yml' ]; 24 | 25 | return fetch( gistConfig.raw_url ).then( res => { 26 | 27 | if ( ! res.ok ) 28 | throw new Http( 500, 'Configuration retrieval failed' ); 29 | 30 | return res.text( ); 31 | 32 | } ).then( configText => { 33 | 34 | return YAML.parse( configText ); 35 | 36 | } ).then( config => { 37 | 38 | return { gist, config }; 39 | 40 | }, error => { 41 | 42 | throw new Http( 500, 'Invalid configuration' ); 43 | 44 | } ).catch( error => { 45 | 46 | throw new Http( 500, 'Unexpected error' ); 47 | 48 | } ); 49 | 50 | } ); 51 | 52 | } 53 | 54 | var app = express( ); 55 | 56 | app.use( bodyParser.text( { type : '*/*' } ) ); 57 | 58 | app.use( ( req, res, next ) => { 59 | 60 | res.header( 'Access-Control-Allow-Origin', '*' ); 61 | res.header( 'Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS' ); 62 | res.header( 'Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept' ); 63 | 64 | next( ); 65 | 66 | } ); 67 | 68 | app.get( '/:uid/:file', ( req, res ) => { 69 | 70 | getGistConfig( req.params.uid ).then( ( { gist, config } ) => { 71 | 72 | if ( ! Object.hasOwnProperty.call( config.files, req.params.file ) ) 73 | throw new Http( 403, 'Unlisted file' ); 74 | 75 | if ( ! [ 'all', 'readonly' ].includes( config.files[ req.params.file ] ) ) 76 | throw new Http( 403, 'Cannot access file' ); 77 | 78 | if ( ! Object.hasOwnProperty.call( gist.files, req.params.file ) ) 79 | throw new Http( 403, 'File not found' ); 80 | 81 | let gistFile = gist.files[ req.params.file ]; 82 | 83 | console.log( `Fetching /${req.params.uid}/${req.params.file}` ); 84 | 85 | return fetch( gistFile.raw_url ).then( file => { 86 | 87 | if ( ! file.ok ) 88 | throw new Http( 500, 'File retrieval failed' ); 89 | 90 | return file.text( ); 91 | 92 | } ).then( fileText => { 93 | 94 | return res.contentType( gistFile.type ).send( fileText ); 95 | 96 | } ); 97 | 98 | } ).catch( error => { 99 | 100 | if ( error.status ) 101 | res.status( error.status ); 102 | 103 | res.send( error.message ); 104 | 105 | } ); 106 | 107 | } ); 108 | 109 | app.put( '/:uid/:file', ( req, res ) => { 110 | 111 | getGistConfig( req.params.uid ).then( ( { gist, config } ) => { 112 | 113 | if ( ! Object.hasOwnProperty.call( config.files, req.params.file ) ) 114 | throw new Http( 403, 'Unlisted file' ); 115 | 116 | if ( ! [ 'all', 'writeonly' ].includes( config.files[ req.params.file ] ) ) 117 | throw new Http( 403, 'Cannot access file' ); 118 | 119 | if ( ! Object.hasOwnProperty.call( gist.files, req.params.file ) ) 120 | throw new Http( 403, 'File not found' ); 121 | 122 | let gistFile = gist.files[ req.params.file ]; 123 | 124 | console.log( `Updating /${req.params.uid}/${req.params.file}` ); 125 | 126 | return Gist.edit( user, password, req.params.uid, req.params.file, req.body ).then( fileText => { 127 | return res.contentType( gistFile.type ).send( fileText ); 128 | } ); 129 | 130 | } ).catch( error => { 131 | 132 | if ( error.status ) 133 | res.status( error.status ); 134 | 135 | res.send( error.message ); 136 | 137 | } ); 138 | 139 | } ); 140 | 141 | let server = app.listen( port, ( ) => { 142 | 143 | console.log( `Now listening on port ${server.address().port}` ); 144 | 145 | } ); 146 | --------------------------------------------------------------------------------