├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── cli-screenshot.png ├── cli.js ├── documentation ├── README.md.html ├── cli.coffee.html ├── cli.js.html ├── cli.sh.html ├── doc-filelist.js ├── doc-script.js ├── doc-style.css ├── example.coffee.html ├── example.js.html ├── host.coffee.html └── host.js.html ├── example.js ├── header.png ├── header.psd ├── lib ├── api.js └── host.js ├── main.js ├── package.json └── test ├── helper.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | coverage.html -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '4' 5 | after_script: make test-cov 6 | env: 7 | matrix: 8 | - COVERALLS_SERVICE_NAME=travis-ci 9 | global: 10 | secure: sa3dK+Avi/dcFaUZ/8bquBCAe0nUQ/yVusvzw5joUpJpzrIJKlIxV86bLa58cJAZSNGNyl/ZcjUaejR4xKyjJuYuLsOqN9p3tAY24AgnSf1Tr8EH3ML0k8e0HdOtmbEo9Rn742XSxN0VcPoXgeJgM0vigTJ66iqav6sJ2DMPKDQ= 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test 2 | REPORTER = spec 3 | XML_FILE = reports/TEST-all.xml 4 | HTML_FILE = reports/coverage.html 5 | ssl = GIT_SSL_NO_VERIFY=true 6 | 7 | test: test-mocha 8 | 9 | test-ci: 10 | $(MAKE) test-mocha REPORTER=xUnit > $(XML_FILE) 11 | 12 | test-all: clean test-ci test-cov 13 | 14 | test-mocha: 15 | @echo ${ssl} 16 | @NODE_ENV=test mocha \ 17 | --timeout 10000 \ 18 | --reporter ${REPORTER} \ 19 | $(TESTS) 20 | 21 | test-cov: 22 | @echo TRAVIS_JOB_ID=$(TRAVIS_JOB_ID) 23 | @echo ${ssl} 24 | @NODE_ENV=test mocha \ 25 | --timeout 10000 \ 26 | --require blanket \ 27 | --reporter mocha-lcov-reporter | ./node_modules/coveralls/bin/coveralls.js \ 28 | $(TESTS) 29 | 30 | clean: 31 | rm -f reports/* 32 | rm -fr lib-cov -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeJS Git Server 2 | 3 |  4 | [](https://travis-ci.org/stackdot/NodeJS-Git-Server) [](https://coveralls.io/github/qrpike/NodeJS-Git-Server?branch=master) 5 | 6 | A multi-tenant git server using NodeJS. 7 | 8 | Read the [Documented Source Code Here](http://qrpike.github.io/NodeJS-Git-Server/host.coffee.html) 9 | 10 | Made to be able to support many git repo's and users with Read/Write customizable permissions. 11 | 12 | 13 | # Install Git Server 14 | 15 | To install the git server run: 16 | 17 | npm install git-server 18 | 19 | To run tests 20 | 21 | git clone https://github.com/qrpike/NodeJS-Git-Server.git 22 | cd ./NodeJS-Git-Server 23 | make test 24 | 25 | 26 | # Example Usage 27 | 28 | The GitServer is a very easy to get up and running git server. It uses the [Pushover](https://github.com/substack/pushover) and [git-emit](https://github.com/substack/node-git-emit) modules for listening to git events, and its own layer to do the security for each repo & user. 29 | 30 | ```javascript 31 | var GitServer = require('git-server'); 32 | var newUser = { 33 | username:'demo', 34 | password:'demo' 35 | } 36 | var newRepo = { 37 | name:'myrepo', 38 | anonRead:false, 39 | users: [ 40 | { user:newUser, permissions:['R','W'] } 41 | ] 42 | } 43 | server = new GitServer({repos: [ newRepo ]}); 44 | ``` 45 | 46 | # Events: 47 | 48 | Server object emits these events: 49 | 50 | ##passive events 51 | 52 | These events are informational only. They can not be aborted. 53 | 54 | * post-applypatch 55 | * post-commit 56 | * post-checkout 57 | * post-merge 58 | * post-receive 59 | * post-update 60 | * post-rewrite 61 | 62 | ##abortable events 63 | 64 | These events can be aborted or accepted. If there will be no listeners for any of these events, they will be automatically accepted. If object can be aborted, it will have canAbort property in update argument. 65 | 66 | * fetch 67 | * commit 68 | * applypatch-msg 69 | * pre-applypatch 70 | * pre-commit 71 | * prepare-commit-msg 72 | * commit-msg 73 | * pre-rebase 74 | * pre-receive 75 | * update 76 | * pre-auto-gc 77 | 78 | ```javascript 79 | var GitServer = require('git-server'); 80 | var newUser = { 81 | username:'demo', 82 | password:'demo' 83 | } 84 | var newRepo = { 85 | name:'myrepo', 86 | anonRead:false, 87 | users: [ 88 | { user:newUser, permissions:['R','W'] } 89 | ] 90 | } 91 | server = new GitServer({repos: [ newRepo ]}); 92 | server.on('commit', function(update, repo) { 93 | // do some logging or other stuff 94 | update.accept() //accept the update. 95 | }); 96 | server.on('post-update', function(update, repo) { 97 | //do some deploy stuff 98 | }); 99 | ``` 100 | 101 | When we start the git server, it will default to port 7000. We can test this using git on this (or another ) machine. 102 | 103 | ``` 104 | git clone http://localhost:7000/myrepo.git 105 | ``` 106 | 107 | Since this repo does *NOT* allow anonymous reading, it will prompt us for a user/pass 108 | 109 | To make this faster, we can use the basic auth structure: 110 | 111 | git clone http://demo:demo@localhost:7000/myrepo.git 112 | 113 | This should not prompt you for any user/pass. Also in the future when you push changes, or pull, it will not ask you for this info again. 114 | 115 | ## Repo object 116 | 117 | Repo object is the object passed to start the server plus some additional methods and properties. 118 | 119 | ```javascript 120 | { 121 | name: 'stackdot', 122 | anonRead: false, 123 | users: [ { user: {username: "demo", password: "demo"}, permissions: ["R", "W"] } ], 124 | path: '/tmp/repos/stackdot.git', 125 | last_commit: { 126 | status: 'pending', 127 | repo: 'anon.git', 128 | service: 'receive-pack', 129 | cwd: '/tmp/repos/stackdot.git', 130 | last: '00960000000000000000000000000000000000000000', 131 | commit: '67359bb4a6cddd97b59507413542e0b08ef078b0', 132 | evName: 'push', 133 | branch: 'master' 134 | } 135 | } 136 | ``` 137 | 138 | ## Update object 139 | 140 | `update` is an http duplex object (see below) with these extra properties: 141 | 142 | ```javascript 143 | { 144 | cwd: '/tmp/repos/stackdot.git', // Current repo dir 145 | repo: 'stackdot.git', // Repo name 146 | accept: [Function], // Method to accept request (if aplicable) 147 | reject: [Function], // Method to reject request (if aplicable) 148 | exists: true, // Does the repo exist 149 | name: 'fetch', // Event name 150 | canAbort: true // If event can be abbortable 151 | } 152 | ``` 153 | 154 | # HTTPS 155 | 156 | The ability to use HTTPS is now implemented for the module and the cli. This is important so that your username & password is encrypted when being sent over the wire. If you are not using username/password then you may want to disregard this section. 157 | 158 | To enable HTTPS in the module, use the 'cert' param: 159 | 160 | ```javascript 161 | var fs = require('fs'); 162 | var certs = { 163 | key : fs.readFileSync('../certs/privatekey.pem') 164 | cert : fs.readFileSync('../certs/certificate.pem') 165 | }; 166 | _g = new GitServer({repos: [ newRepo ]}, undefined, undefined, undefined, certs); 167 | ``` 168 | 169 | To enable HTTPS in the cli, use the '--ssl' option along with '--key' and '--cert' options: 170 | 171 | git-server[|gitserver] --ssl --key ../certs/privatekey.pem --cert ../certs/certificate.pem 172 | 173 | To create these certs you can run: 174 | 175 | openssl genrsa -out privatekey.pem 1024 176 | openssl req -new -key privatekey.pem -out certrequest.csr 177 | openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem 178 | 179 | Also, be aware that when using HTTPS for the git server, when you try to clone,etc. It will give you an SSL error because git (which uses CURL) cannot verify the SSL Cert. To correct this, install a actual, verified SSL Cert ( Free ones here: [StartCom](http://www.startssl.com/?app=1) ) 180 | 181 | If you want to keep using the self signed cert like we created above ^ just tell git to not verify the cert. ( Other ways to do it [here](http://www.f15ijp.com/2012/08/git-ssl-certificate-problem-how-to-turn-off-ssl-validation-for-a-repo/) ) 182 | 183 | export GIT_SSL_NO_VERIFY=true 184 | 185 | And you are good to go! 186 | 187 | # CLI Usage 188 | 189 | 190 | When you install this package globally using 191 | 192 | ``` 193 | sudo npm install -g git-server 194 | ``` 195 | 196 | You will now have a CLI interface you can run and interact with. 197 | 198 | Get started by typing `git-server` or `gitserver` into your terminal. 199 | 200 | You should see something similar to this: 201 |  202 | 203 | With this interface you can type the following to see the available commands: 204 | 205 | git-server> help 206 | 207 | You will see a list of possible commands, just enter a command and the prompt will ask you for any additional details needed. 208 | 209 | # TODO Items 210 | 211 | - Make YouTube demo of the app 212 | 213 | ### This is a work in progress - please feel free to contribute! 214 | please contribute 215 | #License 216 | 217 | (The MIT License) 218 | 219 | Copyright (c) 2016 [Quinton Pike](https://twitter.com/QuintonPike) 220 | 221 | 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: 222 | 223 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 224 | 225 | 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. 226 | -------------------------------------------------------------------------------- /cli-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackdot/NodeJS-Git-Server/3be1aa65d428568dcd105d7c9062ab90260b10e1/cli-screenshot.png -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Generated by CoffeeScript 1.6.1 3 | /* 4 | This is the CLI interface for using git-server. 5 | */ 6 | 7 | var CLI, EventEmitter, GITCLI, GitServer, Table, async, commander, certs, fs, getUserHomeDir, logging, mkdirp, path, repoDB, repoLocation, repoPort, repos, _c, _g, 8 | _this = this, 9 | __hasProp = {}.hasOwnProperty, 10 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 11 | 12 | EventEmitter = require('events').EventEmitter; 13 | 14 | CLI = require('cli-listener'); 15 | 16 | GitServer = require('./lib/host.js'); 17 | 18 | mkdirp = require('mkdirp'); 19 | 20 | fs = require('fs'); 21 | 22 | async = require('async'); 23 | 24 | path = require('path'); 25 | 26 | Table = require('cli-table'); 27 | 28 | commander = require('commander'); 29 | 30 | commander.version('0.2.2') 31 | .option('-p, --port [value]', 'Port to run Git on', parseInt) 32 | .option('-d, --directory [value]', 'Directory of the repos') 33 | .option('-l, --logging', 'Verbose logging on or off') 34 | .option('-s, --ssl', 'Enable SSL support; requires key and cert options') 35 | .option('-k, --key [value]', 'SSL key path (required for ssl)') 36 | .option('-c, --cert [value]', 'SSL cert path (required for ssl)') 37 | .option('-a, --certificate-authority [value]', 'SSL certificate authority path') 38 | .parse(process.argv); 39 | 40 | repoPort = commander.port || 7000; 41 | 42 | logging = commander.logging || false; 43 | 44 | if (commander.ssl && commander.key && commander.cert) { 45 | var readFileSync = fs.readFileSync, 46 | resolve = require('path').resolve, 47 | readFile = function(path) { 48 | path = resolve(path); 49 | return readFileSync(path, {encoding:'utf8'}).toString(); 50 | }; 51 | 52 | certs = { 53 | key: readFile(commander.key), 54 | cert: readFile(commander.cert) 55 | }; 56 | 57 | if(commander.certificateAuthority) { 58 | // Properly concatinate the ca chain for node https 59 | var caChain = function caChain(cc) { 60 | var ca = [], 61 | cert = [], 62 | chain = cc.split('\n'), 63 | _i, _len, line; 64 | 65 | for (_i = 0, _len = chain.length; _i < _len; _i++) { 66 | line = chain[_i]; 67 | if (!(line.length !== 0)) { 68 | continue; 69 | } 70 | cert.push(line); 71 | if (line.match(/-END CERTIFICATE-/)) { 72 | ca.push(cert.join("\n")); 73 | cert = []; 74 | } 75 | } 76 | 77 | return ca; 78 | }; 79 | certs.ca = caChain(readFile(commander.certificateAuthority)); 80 | } 81 | } 82 | 83 | getUserHomeDir = function() { 84 | var dir; 85 | if (process.platform === 'win32') { 86 | dir = 'USERPROFILE'; 87 | } else { 88 | dir = 'HOME'; 89 | } 90 | return process.env[dir]; 91 | }; 92 | 93 | repoLocation = commander.directory || path.join(getUserHomeDir(), './git-server/repos'); 94 | 95 | if (commander.directory !== void 0) { 96 | repoDB = commander.directory + '.db'; 97 | } else { 98 | repoDB = path.join(getUserHomeDir(), './git-server/repos.db'); 99 | } 100 | 101 | mkdirp.sync(repoLocation); 102 | 103 | if (fs.existsSync(repoDB)) { 104 | repos = JSON.parse(fs.readFileSync(repoDB)); 105 | } else { 106 | repos = { 107 | repos: [], 108 | users: [] 109 | }; 110 | } 111 | 112 | GITCLI = (function(_super) { 113 | 114 | __extends(GITCLI, _super); 115 | 116 | /* 117 | Constructor for the CLI interface 118 | @param {Object} gitServer Git-Server object instance 119 | @param {Array} users Users we are managing 120 | */ 121 | 122 | 123 | function GITCLI(gitServer, users) { 124 | var availableCalls, welcomeMessage, 125 | _this = this; 126 | this.gitServer = gitServer; 127 | this.users = users != null ? users : []; 128 | this.saveConfig = function() { 129 | return GITCLI.prototype.saveConfig.apply(_this, arguments); 130 | }; 131 | this.listRepos = function(callback) { 132 | return GITCLI.prototype.listRepos.apply(_this, arguments); 133 | }; 134 | this.listUsers = function(callback) { 135 | return GITCLI.prototype.listUsers.apply(_this, arguments); 136 | }; 137 | this.columnPercentage = function(percentage) { 138 | return GITCLI.prototype.columnPercentage.apply(_this, arguments); 139 | }; 140 | this.getUser = function(username) { 141 | return GITCLI.prototype.getUser.apply(_this, arguments); 142 | }; 143 | this.addUserToRepo = function(callback) { 144 | return GITCLI.prototype.addUserToRepo.apply(_this, arguments); 145 | }; 146 | this.createUser = function(callback) { 147 | return GITCLI.prototype.createUser.apply(_this, arguments); 148 | }; 149 | this.createRepo = function(callback) { 150 | return GITCLI.prototype.createRepo.apply(_this, arguments); 151 | }; 152 | availableCalls = { 153 | 'create repo': this.createRepo, 154 | 'create user': this.createUser, 155 | 'list repos': this.listRepos, 156 | 'list users': this.listUsers, 157 | 'add user to repo': this.addUserToRepo 158 | }; 159 | welcomeMessage = "Welcome to Git Server - Powered by NodeJS\n - Repo Location: " + repoLocation + "\n - Listening Port: " + repoPort + "\n - Repo Count: " + this.gitServer.repos.length + "\n - User Count: " + this.users.length; 160 | this.cli = new CLI('git-server', welcomeMessage, availableCalls); 161 | this.on('changedData', this.saveConfig); 162 | setTimeout(this.cli.resetInput, 100); 163 | } 164 | 165 | GITCLI.prototype.createRepo = function(callback) { 166 | var _this = this; 167 | return this.cli.ask({ 168 | name: 'Repo Name: ', 169 | anonRead: 'Anonymous Access? [y,N] :: ' 170 | }, function(err, results) { 171 | var anon, name; 172 | if (err) { 173 | throw err; 174 | } 175 | name = results.name.toLowerCase(); 176 | anon = results.anonRead.toLowerCase(); 177 | if (anon === 'y') { 178 | anon = true; 179 | } else { 180 | anon = false; 181 | } 182 | _this.gitServer.createRepo({ 183 | name: name, 184 | anonRead: anon, 185 | users: [] 186 | }); 187 | _this.emit('changedData'); 188 | return callback(); 189 | }); 190 | }; 191 | 192 | GITCLI.prototype.createUser = function(callback) { 193 | var _this = this; 194 | return this.cli.ask({ 195 | username: 'Users username: ', 196 | password: 'Users password: ' 197 | }, function(err, answers) { 198 | var user, username; 199 | if (err) { 200 | throw err; 201 | } 202 | username = answers.username.toLowerCase(); 203 | user = _this.getUser(username); 204 | if (user !== false) { 205 | console.log('This username already exists'); 206 | return callback(); 207 | } else { 208 | user = { 209 | username: username, 210 | password: answers.password 211 | }; 212 | _this.users.push(user); 213 | _this.emit('changedData'); 214 | return callback(); 215 | } 216 | }); 217 | }; 218 | 219 | GITCLI.prototype.addUserToRepo = function(callback) { 220 | var _this = this; 221 | return this.cli.ask({ 222 | repoName: 'Repo Name: ', 223 | username: 'Users username: ', 224 | permissions: 'Permissions (comma seperated: R,W ): ' 225 | }, function(err, answers) { 226 | var permissions, repo, repoName, user, username; 227 | repoName = answers.repoName.toLowerCase(); 228 | username = answers.username.toLowerCase(); 229 | repo = _this.gitServer.getRepo(repoName + '.git'); 230 | user = _this.getUser(username); 231 | permissions = answers.permissions.split(','); 232 | if (permissions.length === 0) { 233 | permissions = ['R']; 234 | } 235 | if (repo === false) { 236 | return console.log('Repo doesnt exist.'); 237 | } else if (user === false) { 238 | return console.log('User doesnt exist.'); 239 | } else { 240 | repo.users.push({ 241 | user: user, 242 | permissions: permissions 243 | }); 244 | _this.emit('changedData'); 245 | return callback(); 246 | } 247 | }); 248 | }; 249 | 250 | /* 251 | Loop through and find this user 252 | @param {String} username Username of the user we are looking for 253 | */ 254 | 255 | 256 | GITCLI.prototype.getUser = function(username) { 257 | var user, _i, _len, _ref; 258 | _ref = this.users; 259 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 260 | user = _ref[_i]; 261 | if (user.username === username) { 262 | return user; 263 | } 264 | } 265 | return false; 266 | }; 267 | 268 | /* 269 | Get the number of columns needed from a % width 270 | @param {Int} percentage Percentage of the console width 271 | */ 272 | 273 | 274 | GITCLI.prototype.columnPercentage = function(percentage) { 275 | return Math.floor(process.stdout.columns * (percentage / 100)); 276 | }; 277 | 278 | GITCLI.prototype.listUsers = function(callback) { 279 | var repo, repoUser, table, user, users, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3; 280 | users = this.users; 281 | _ref = this.users; 282 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 283 | user = _ref[_i]; 284 | user.repos = []; 285 | _ref1 = this.gitServer.repos; 286 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 287 | repo = _ref1[_j]; 288 | _ref2 = repo.users; 289 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 290 | repoUser = _ref2[_k]; 291 | if (repoUser.user.username === user.username) { 292 | user.repos.push({ 293 | name: repo.name, 294 | permissions: repoUser.permissions 295 | }); 296 | } 297 | } 298 | } 299 | } 300 | table = new Table({ 301 | head: ['Username', 'Password', 'Repos'], 302 | colWidths: [this.columnPercentage(40) - 1, this.columnPercentage(20) - 1, this.columnPercentage(40) - 1] 303 | }); 304 | _ref3 = this.users; 305 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { 306 | user = _ref3[_l]; 307 | repos = (function() { 308 | var _len4, _m, _ref4, _results; 309 | _ref4 = user.repos; 310 | _results = []; 311 | for (_m = 0, _len4 = _ref4.length; _m < _len4; _m++) { 312 | repo = _ref4[_m]; 313 | _results.push("" + repo.name + " (" + (repo.permissions.join(',')) + ")"); 314 | } 315 | return _results; 316 | })(); 317 | table.push([user.username, user.password, repos.join('\n')]); 318 | } 319 | console.log(table.toString()); 320 | return callback(); 321 | }; 322 | 323 | GITCLI.prototype.listRepos = function(callback) { 324 | var repo, table, user, users, _i, _len; 325 | repos = this.gitServer.repos; 326 | table = new Table({ 327 | head: ['Repo Name', 'Anonymous Reads', 'Users'], 328 | colWidths: [this.columnPercentage(40) - 1, this.columnPercentage(20) - 1, this.columnPercentage(40) - 1] 329 | }); 330 | for (_i = 0, _len = repos.length; _i < _len; _i++) { 331 | repo = repos[_i]; 332 | users = (function() { 333 | var _j, _len1, _ref, _results; 334 | _ref = repo.users; 335 | _results = []; 336 | for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { 337 | user = _ref[_j]; 338 | _results.push("" + user.user.username + " (" + (user.permissions.join(',')) + ")"); 339 | } 340 | return _results; 341 | })(); 342 | table.push([repo.name, repo.anonRead, users.join('\n')]); 343 | } 344 | console.log(table.toString()); 345 | return callback(); 346 | }; 347 | 348 | GITCLI.prototype.saveConfig = function() { 349 | var config; 350 | config = JSON.stringify({ 351 | repos: this.gitServer.repos, 352 | users: this.users 353 | }); 354 | return fs.writeFileSync(repoDB, config); 355 | }; 356 | 357 | return GITCLI; 358 | 359 | })(EventEmitter); 360 | 361 | if (!certs) { 362 | _g = new GitServer(repos.repos, logging, repoLocation, repoPort); 363 | } else { 364 | _g = new GitServer(repos.repos, logging, repoLocation, repoPort, certs); 365 | } 366 | 367 | _c = new GITCLI(_g, repos.users); 368 | -------------------------------------------------------------------------------- /documentation/README.md.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |A multi-tenant git server using NodeJS.
63 | 64 |Read the Documented Source Code Here
65 | 66 |Made to be able to support many git repo's and users with Read/Write customizable permissions.
67 | 68 | 69 |To install the git server run:
78 | 79 | 80 |npm install git-server
81 |
The GitServer is a very easy to get up and running git server. It uses the Pushover module for listening to git events, and its own layer to do the security for each repo & user.
95 | 96 | 97 |var GitServer = require('git-server');
98 | var newUser = {
99 | username:'demo',
100 | password:'demo'
101 | }
102 | var newRepo = {
103 | name:'myrepo',
104 | anonRead:false,
105 | users: [
106 | { user:newUser, permissions:['R','W'] }
107 | ]
108 | }
109 | _g = new GitServer([ newRepo ]);
110 |
If you want onSuccessful triggers, you can add them to each repo like so:
124 | 125 | 126 |var newRepo = {
127 | name:'myrepo',
128 | anonRead:false,
129 | users: [
130 | { user:newUser, permissions:['R','W'] }
131 | ],
132 | onSuccessful : {
133 | fetch : function( repo, method ){
134 | console.log('Successful fetch/pull/clone on repo:',repo.name);
135 | }
136 | push : function( repo, method ){
137 | console.log('PUSHed:', repo.name);
138 | // Possibly do some deploy scripts etc.
139 | }
140 | }
141 | }
142 |
When we start the git server, it will default to port 7000. We can test this using git on this (or another ) machine.
147 | 148 | 149 |git clone http://localhost:7000/myrepo.git
150 |
Since this repo does NOT allow anonymous reading, it will prompt us for a user/pass
155 | 156 |To make this faster, we can use the basic auth structure:
157 | 158 | 159 |git clone http://demo:demo@localhost:7000/myrepo.git
160 |
This should not prompt you for any user/pass. Also in the future when you push changes, or pull, it will not ask you for this info again.
165 | 166 | 167 |The ability to use HTTPS is now implemented for the module (not the cli yet). This is important so that your username & password is encrypted when being sent over the wire. If you are not using username/password then you may want to disregard this section.
176 | 177 |To enable HTTPS, send the module the 'cert' param:
178 | 179 | 180 |var fs = require('fs');
181 | var certs = {
182 | key : fs.readFileSync('../certs/privatekey.pem')
183 | cert : fs.readFileSync('../certs/certificate.pem')
184 | };
185 | _g = new GitServer([ newRepo ], undefined, undefined, undefined, certs);
186 |
To create these certs you can run:
191 | 192 | 193 |openssl genrsa -out privatekey.pem 1024
194 | openssl req -new -key privatekey.pem -out certrequest.csr
195 | openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem
196 |
Also, be aware that when using HTTPS for the git server, when you try to clone,etc. It will give you an SSL error because git (which uses CURL) cannot verify the SSL Cert. To correct this, install a actual, verified SSL Cert ( Free ones here: StartCom )
201 | 202 |If you want to keep using the self signed cert like we created above ^ just tell git to not verify the cert. ( Other ways to do it here )
203 | 204 | 205 |export GIT_SSL_NO_VERIFY=true
206 |
And you are good to go!
211 | 212 | 213 |When you install this package globally using
222 | 223 | 224 |sudo npm install -g git-server
225 |
You will now have a CLI interface you can run and interact with.
230 | 231 |Get started by typing git-server
or gitserver
into your terminal.
You should see something similar to this:
234 |
With this interface you can type the following to see the available commands:
237 | 238 | 239 |git-server> help
240 |
You will see a list of possible commands, just enter a command and the prompt will ask you for any additional details needed.
245 | 246 | 247 |please contribute
271 | 272 | 273 |(The MIT License)
282 | 283 |Copyright (c) 2010 Quinton Pike
284 | 285 |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:
286 | 287 |The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
288 | 289 |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.
29 | cli.coffee30 | |
31 | 32 | |
35 |
36 | ¶
37 |
38 |
39 |
44 | This is the CLI interface for using git-server. 40 |
42 |
43 | |
45 |
46 | |
47 |
50 |
51 | ¶
52 |
53 | Get required packages: 54 | |
55 |
56 | {EventEmitter} = require 'events'
57 | CLI = require 'cli-listener'
58 | GitServer = require './host.js'
59 | mkdirp = require 'mkdirp'
60 | fs = require 'fs'
61 | async = require 'async'
62 | path = require 'path'
63 | Table = require 'cli-table'
64 | commander = require 'commander'
65 |
66 |
67 |
68 |
69 | |
71 |
74 |
75 | ¶
76 |
77 | Ability to pass in certain settings 78 | |
79 |
80 | commander
81 | .version('0.0.1')
82 | .option( '-p, --port [value]', 'Port to run Git on',parseInt)
83 | .option( '-d, --directory [value]', 'Directory of the repos')
84 | .option( '-l, --logging', 'Verbose logging on or off')
85 | .parse( process.argv )
86 |
87 |
88 |
89 |
90 | |
92 |
95 |
96 | ¶
97 |
98 | Set the port to either -p passed in, or fall back to port 7000 99 | |
100 |
101 | repoPort = commander.port || 7000
102 | logging = commander.logging || false # set logging too
103 |
104 |
105 |
106 |
107 | |
109 |
112 |
113 | ¶
114 |
115 | Get this users home directory if we didnt pass in where the repo location is 116 | |
117 |
118 | getUserHomeDir = ()->
119 | if process.platform is 'win32'
120 | dir = 'USERPROFILE'
121 | else dir = 'HOME'
122 | process.env[dir]
123 |
124 |
125 |
126 |
127 | |
129 |
132 |
133 | ¶
134 |
135 | Set the repo location and repo.db file 136 | |
137 |
138 | repoLocation = commander.directory || path.join( getUserHomeDir(), './git-server/repos' )
139 | if commander.directory != undefined
140 | repoDB = commander.directory+'.db'
141 | else
142 | repoDB = path.join( getUserHomeDir(), './git-server/repos.db' )
143 |
144 |
145 |
146 |
147 | |
149 |
152 |
153 | ¶
154 |
155 | Create the folders if they dont exist 156 | |
157 |
158 | mkdirp.sync repoLocation
159 |
160 |
161 |
162 |
163 | |
165 |
168 |
169 | ¶
170 |
171 | If we have a .db file use the data in it, otherwise use a blank object 172 | |
173 |
174 | if fs.existsSync repoDB
175 | repos = JSON.parse( fs.readFileSync( repoDB ) )
176 | else repos = { repos:[], users:[] }
177 |
178 |
179 |
180 |
181 |
182 | |
184 |
187 |
188 | ¶
189 |
190 | GITCLI Class 191 | |
192 |
193 | class GITCLI extends EventEmitter
194 |
195 |
196 |
197 |
198 |
199 | |
201 |
204 |
205 | ¶
206 |
207 |
208 |
226 | Constructor for the CLI interface 209 |
211 |
212 |
213 |
225 | Params
214 |
215 | gitServer
216 | Object
217 | Git-Server object instance
218 |
219 |
220 | users
221 | Array
222 | Users we are managing
223 |
224 | |
227 |
228 | constructor: ( @gitServer, @users = [] )->
229 |
230 |
231 | |
233 |
236 |
237 | ¶
238 |
239 | Available calls the user can make in the CLI 240 | |
241 |
242 | availableCalls =
243 | 'create repo' : @createRepo
244 | 'create user' : @createUser
245 | 'list repos' : @listRepos
246 | 'list users' : @listUsers
247 | 'add user to repo' : @addUserToRepo
248 |
249 |
250 | |
252 |
255 |
256 | ¶
257 |
258 | Our fabulous welcome message 259 | |
260 |
261 | welcomeMessage = """
262 | Welcome to Git Server - Powered by NodeJS
263 | - Repo Location: #{repoLocation}
264 | - Listening Port: #{repoPort}
265 | - Repo Count: #{@gitServer.repos.length}
266 | - User Count: #{@users.length}
267 | """
268 |
269 |
270 | |
272 |
275 |
276 | ¶
277 |
278 | Create the CLI Object 279 | |
280 |
281 | @cli = new CLI 'git-server', welcomeMessage, availableCalls
282 |
283 |
284 | |
286 |
289 |
290 | ¶
291 |
292 | If we trigger a |
294 |
295 | @on 'changedData', @saveConfig
296 |
297 |
298 | |
300 |
303 |
304 | ¶
305 |
306 | Little hack to reset the input after the gitServer logs any messages. 307 | |
308 |
309 | setTimeout @cli.resetInput, 100
310 |
311 |
312 |
313 | |
315 |
318 |
319 | ¶
320 |
321 | Create a new repo 322 | |
323 |
324 | createRepo: ( callback )=>
325 | @cli.ask { name:'Repo Name: ', anonRead:'Anonymous Access? [y,N] :: ' }, ( err, results )=>
326 | if err then throw err
327 | name = results.name.toLowerCase()
328 | anon = results.anonRead.toLowerCase()
329 | if anon is 'y' then anon = true
330 | else anon = false
331 | @gitServer.createRepo name:name, anonRead:anon, users:[]
332 | @emit 'changedData'
333 | callback()
334 |
335 |
336 |
337 |
338 |
339 | |
341 |
344 |
345 | ¶
346 |
347 | Create a new user 348 | |
349 |
350 | createUser: ( callback )=>
351 | @cli.ask { username:'Users username: ', password:'Users password: ' }, ( err, answers )=>
352 | if err then throw err
353 | username = answers.username.toLowerCase()
354 | user = @getUser username
355 | if user != false
356 | console.log 'This username already exists'
357 | callback()
358 | else
359 | user =
360 | username : username
361 | password : answers.password
362 | @users.push user
363 | @emit 'changedData'
364 | callback()
365 |
366 |
367 |
368 |
369 |
370 | |
372 |
375 |
376 | ¶
377 |
378 | Add a user to a repo 379 | |
380 |
381 | addUserToRepo: ( callback )=>
382 | @cli.ask { repoName:'Repo Name: ', username:'Users username: ', permissions: 'Permissions (comma seperated: R,W ): ' }, ( err, answers )=>
383 | repoName = answers.repoName.toLowerCase()
384 | username = answers.username.toLowerCase()
385 | repo = @gitServer.getRepo repoName+'.git'
386 | user = @getUser username
387 | permissions = answers.permissions.split(',')
388 | permissions = ['R'] if permissions.length is 0
389 | if repo is false
390 | console.log 'Repo doesnt exist.'
391 | else if user is false
392 | console.log 'User doesnt exist.'
393 | else
394 | repo.users.push
395 | user: user
396 | permissions: permissions
397 | @emit 'changedData'
398 | callback()
399 |
400 |
401 |
402 |
403 | |
405 |
408 |
409 | ¶
410 |
411 |
412 |
425 | Loop through and find this user 413 |
415 |
416 |
417 |
424 | Params
418 |
419 | username
420 | String
421 | Username of the user we are looking for
422 |
423 | |
426 |
427 | getUser: ( username )=>
428 | for user in @users
429 | return user if user.username is username
430 | false
431 |
432 |
433 |
434 | |
436 |
439 |
440 | ¶
441 |
442 |
443 |
456 | Get the number of columns needed from a % width 444 |
446 |
447 |
448 |
455 | Params
449 |
450 | percentage
451 | Int
452 | Percentage of the console width
453 |
454 | |
457 |
458 | columnPercentage: ( percentage )=>
459 | Math.floor process.stdout.columns*( percentage/100 )
460 |
461 |
462 |
463 |
464 |
465 | |
467 |
470 |
471 | ¶
472 |
473 | List out all the current users and their associated repo's & permissions 474 | |
475 |
476 | listUsers: ( callback )=>
477 | users = @users
478 | for user in @users
479 | user.repos = []
480 | for repo in @gitServer.repos
481 | for repoUser in repo.users
482 | if repoUser.user.username is user.username
483 | user.repos.push
484 | name: repo.name
485 | permissions: repoUser.permissions
486 |
487 |
488 | |
490 |
493 |
494 | ¶
495 |
496 | create new console table 497 | |
498 |
499 | table = new Table
500 | head:['Username','Password','Repos']
501 | colWidths: [@columnPercentage(40)-1,@columnPercentage(20)-1,@columnPercentage(40)-1]
502 |
503 |
504 | |
506 |
509 |
510 | ¶
511 |
512 | Fill up the table 513 | |
514 |
515 | for user in @users
516 | repos = for repo in user.repos
517 | "#{repo.name} (#{repo.permissions.join(',')})"
518 | table.push [ user.username, user.password, repos.join('\n') ]
519 |
520 |
521 | |
523 |
526 |
527 | ¶
528 |
529 | log it 530 | |
531 |
532 | console.log table.toString()
533 | callback()
534 |
535 |
536 |
537 |
538 |
539 | |
541 |
544 |
545 | ¶
546 |
547 | List out all the repo's and their associated users 548 | |
549 |
550 | listRepos: ( callback )=>
551 | repos = @gitServer.repos
552 |
553 | |
555 |
558 |
559 | ¶
560 |
561 | Create a new table 562 | |
563 |
564 | table = new Table
565 | head:['Repo Name','Anonymous Reads','Users']
566 | colWidths: [@columnPercentage(40)-1,@columnPercentage(20)-1,@columnPercentage(40)-1]
567 |
568 |
569 | |
571 |
574 |
575 | ¶
576 |
577 | Fill up the table 578 | |
579 |
580 | for repo in repos
581 | users = for user in repo.users
582 | "#{user.user.username} (#{user.permissions.join(',')})"
583 | table.push [ repo.name, repo.anonRead, users.join('\n') ]
584 |
585 |
586 | |
588 |
591 |
592 | ¶
593 |
594 | log it 595 | |
596 |
597 | console.log table.toString()
598 | callback()
599 |
600 |
601 |
602 |
603 |
604 | |
606 |
609 |
610 | ¶
611 |
612 | Save the data to the .db file 613 | |
614 |
615 | saveConfig: ()=>
616 | config = JSON.stringify({ repos:@gitServer.repos, users:@users })
617 | fs.writeFileSync repoDB, config
618 |
619 |
620 |
621 |
622 | |
624 |
627 |
628 | ¶
629 |
630 | Start me up buttercup 631 | |
632 |
633 | _g = new GitServer repos.repos, logging, repoLocation, repoPort
634 | _c = new GITCLI _g, repos.users
635 | |
637 |
29 | cli.sh30 | |
31 | 32 | |
35 |
36 | ¶
37 |
38 |
39 | |
40 | #!/usr/bin/env node
41 | // Generated by CoffeeScript 1.6.1
42 | /*
43 | This is the CLI interface for using git-server.
44 | */
45 |
46 | var CLI, EventEmitter, GITCLI, GitServer, Table, async, commander, fs, getUserHomeDir, logging, mkdirp, path, repoDB, repoLocation, repoPort, repos, _c, _g,
47 | _this = this,
48 | __hasProp = {}.hasOwnProperty,
49 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
50 |
51 | EventEmitter = require('events').EventEmitter;
52 |
53 | CLI = require('cli-listener');
54 |
55 | GitServer = require('./host.js');
56 |
57 | mkdirp = require('mkdirp');
58 |
59 | fs = require('fs');
60 |
61 | async = require('async');
62 |
63 | path = require('path');
64 |
65 | Table = require('cli-table');
66 |
67 | commander = require('commander');
68 |
69 | commander.version('0.0.1').option('-p, --port [value]', 'Port to run Git on', parseInt).option('-d, --directory [value]', 'Directory of the repos').option('-l, --logging', 'Verbose logging on or off').parse(process.argv);
70 |
71 | repoPort = commander.port || 7000;
72 |
73 | logging = commander.logging || false;
74 |
75 | getUserHomeDir = function() {
76 | var dir;
77 | if (process.platform === 'win32') {
78 | dir = 'USERPROFILE';
79 | } else {
80 | dir = 'HOME';
81 | }
82 | return process.env[dir];
83 | };
84 |
85 | repoLocation = commander.directory || path.join(getUserHomeDir(), './git-server/repos');
86 |
87 | if (commander.directory !== void 0) {
88 | repoDB = commander.directory + '.db';
89 | } else {
90 | repoDB = path.join(getUserHomeDir(), './git-server/repos.db');
91 | }
92 |
93 | mkdirp.sync(repoLocation);
94 |
95 | if (fs.existsSync(repoDB)) {
96 | repos = JSON.parse(fs.readFileSync(repoDB));
97 | } else {
98 | repos = {
99 | repos: [],
100 | users: []
101 | };
102 | }
103 |
104 | GITCLI = (function(_super) {
105 |
106 | __extends(GITCLI, _super);
107 |
108 | /*
109 | Constructor for the CLI interface
110 | @param {Object} gitServer Git-Server object instance
111 | @param {Array} users Users we are managing
112 | */
113 |
114 |
115 | function GITCLI(gitServer, users) {
116 | var availableCalls, welcomeMessage,
117 | _this = this;
118 | this.gitServer = gitServer;
119 | this.users = users != null ? users : [];
120 | this.saveConfig = function() {
121 | return GITCLI.prototype.saveConfig.apply(_this, arguments);
122 | };
123 | this.listRepos = function(callback) {
124 | return GITCLI.prototype.listRepos.apply(_this, arguments);
125 | };
126 | this.listUsers = function(callback) {
127 | return GITCLI.prototype.listUsers.apply(_this, arguments);
128 | };
129 | this.columnPercentage = function(percentage) {
130 | return GITCLI.prototype.columnPercentage.apply(_this, arguments);
131 | };
132 | this.getUser = function(username) {
133 | return GITCLI.prototype.getUser.apply(_this, arguments);
134 | };
135 | this.addUserToRepo = function(callback) {
136 | return GITCLI.prototype.addUserToRepo.apply(_this, arguments);
137 | };
138 | this.createUser = function(callback) {
139 | return GITCLI.prototype.createUser.apply(_this, arguments);
140 | };
141 | this.createRepo = function(callback) {
142 | return GITCLI.prototype.createRepo.apply(_this, arguments);
143 | };
144 | availableCalls = {
145 | 'create repo': this.createRepo,
146 | 'create user': this.createUser,
147 | 'list repos': this.listRepos,
148 | 'list users': this.listUsers,
149 | 'add user to repo': this.addUserToRepo
150 | };
151 | welcomeMessage = "Welcome to Git Server - Powered by NodeJS\n - Repo Location: " + repoLocation + "\n - Listening Port: " + repoPort + "\n - Repo Count: " + this.gitServer.repos.length + "\n - User Count: " + this.users.length;
152 | this.cli = new CLI('git-server', welcomeMessage, availableCalls);
153 | this.on('changedData', this.saveConfig);
154 | }
155 |
156 | GITCLI.prototype.createRepo = function(callback) {
157 | var _this = this;
158 | return this.cli.ask({
159 | name: 'Repo Name: ',
160 | anonRead: 'Anonymous Access? [y,N] :: '
161 | }, function(err, results) {
162 | var anon, name;
163 | if (err) {
164 | throw err;
165 | }
166 | name = results.name.toLowerCase();
167 | anon = results.anonRead.toLowerCase();
168 | if (anon === 'y') {
169 | anon = true;
170 | } else {
171 | anon = false;
172 | }
173 | _this.gitServer.createRepo({
174 | name: name,
175 | anonRead: anon,
176 | users: []
177 | });
178 | _this.emit('changedData');
179 | return callback();
180 | });
181 | };
182 |
183 | GITCLI.prototype.createUser = function(callback) {
184 | var _this = this;
185 | return this.cli.ask({
186 | username: 'Users username: ',
187 | password: 'Users password: '
188 | }, function(err, answers) {
189 | var user, username;
190 | if (err) {
191 | throw err;
192 | }
193 | username = answers.username.toLowerCase();
194 | user = _this.getUser(username);
195 | if (user !== false) {
196 | console.log('This username already exists');
197 | return callback();
198 | } else {
199 | user = {
200 | username: username,
201 | password: answers.password
202 | };
203 | _this.users.push(user);
204 | _this.emit('changedData');
205 | return callback();
206 | }
207 | });
208 | };
209 |
210 | GITCLI.prototype.addUserToRepo = function(callback) {
211 | var _this = this;
212 | return this.cli.ask({
213 | repoName: 'Repo Name: ',
214 | username: 'Users username: ',
215 | permissions: 'Permissions (comma seperated: R,W ): '
216 | }, function(err, answers) {
217 | var permissions, repo, repoName, user, username;
218 | repoName = answers.repoName.toLowerCase();
219 | username = answers.username.toLowerCase();
220 | repo = _this.gitServer.getRepo(repoName + '.git');
221 | user = _this.getUser(username);
222 | permissions = answers.permissions.split(',');
223 | if (permissions.length === 0) {
224 | permissions = ['R'];
225 | }
226 | if (repo === false) {
227 | return console.log('Repo doesnt exist.');
228 | } else if (user === false) {
229 | return console.log('User doesnt exist.');
230 | } else {
231 | repo.users.push({
232 | user: user,
233 | permissions: permissions
234 | });
235 | _this.emit('changedData');
236 | return callback();
237 | }
238 | });
239 | };
240 |
241 | /*
242 | Loop through and find this user
243 | @param {String} username Username of the user we are looking for
244 | */
245 |
246 |
247 | GITCLI.prototype.getUser = function(username) {
248 | var user, _i, _len, _ref;
249 | _ref = this.users;
250 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
251 | user = _ref[_i];
252 | if (user.username === username) {
253 | return user;
254 | }
255 | }
256 | return false;
257 | };
258 |
259 | /*
260 | Get the number of columns needed from a % width
261 | @param {Int} percentage Percentage of the console width
262 | */
263 |
264 |
265 | GITCLI.prototype.columnPercentage = function(percentage) {
266 | return Math.floor(process.stdout.columns * (percentage / 100));
267 | };
268 |
269 | GITCLI.prototype.listUsers = function(callback) {
270 | var repo, repoUser, table, user, users, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3;
271 | users = this.users;
272 | _ref = this.users;
273 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
274 | user = _ref[_i];
275 | user.repos = [];
276 | _ref1 = this.gitServer.repos;
277 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
278 | repo = _ref1[_j];
279 | _ref2 = repo.users;
280 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
281 | repoUser = _ref2[_k];
282 | if (repoUser.user.username === user.username) {
283 | user.repos.push({
284 | name: repo.name,
285 | permissions: repoUser.permissions
286 | });
287 | }
288 | }
289 | }
290 | }
291 | table = new Table({
292 | head: ['Username', 'Password', 'Repos'],
293 | colWidths: [this.columnPercentage(40) - 1, this.columnPercentage(20) - 1, this.columnPercentage(40) - 1]
294 | });
295 | _ref3 = this.users;
296 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) {
297 | user = _ref3[_l];
298 | repos = (function() {
299 | var _len4, _m, _ref4, _results;
300 | _ref4 = user.repos;
301 | _results = [];
302 | for (_m = 0, _len4 = _ref4.length; _m < _len4; _m++) {
303 | repo = _ref4[_m];
304 | _results.push("" + repo.name + " (" + (repo.permissions.join(',')) + ")");
305 | }
306 | return _results;
307 | })();
308 | table.push([user.username, user.password, repos.join('\n')]);
309 | }
310 | console.log(table.toString());
311 | return callback();
312 | };
313 |
314 | GITCLI.prototype.listRepos = function(callback) {
315 | var repo, table, user, users, _i, _len;
316 | repos = this.gitServer.repos;
317 | table = new Table({
318 | head: ['Repo Name', 'Anonymous Reads', 'Users'],
319 | colWidths: [this.columnPercentage(40) - 1, this.columnPercentage(20) - 1, this.columnPercentage(40) - 1]
320 | });
321 | for (_i = 0, _len = repos.length; _i < _len; _i++) {
322 | repo = repos[_i];
323 | users = (function() {
324 | var _j, _len1, _ref, _results;
325 | _ref = repo.users;
326 | _results = [];
327 | for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {
328 | user = _ref[_j];
329 | _results.push("" + user.user.username + " (" + (user.permissions.join(',')) + ")");
330 | }
331 | return _results;
332 | })();
333 | table.push([repo.name, repo.anonRead, users.join('\n')]);
334 | }
335 | console.log(table.toString());
336 | return callback();
337 | };
338 |
339 | GITCLI.prototype.saveConfig = function() {
340 | var config;
341 | config = JSON.stringify({
342 | repos: this.gitServer.repos,
343 | users: this.users
344 | });
345 | return fs.writeFileSync(repoDB, config);
346 | };
347 |
348 | return GITCLI;
349 |
350 | })(EventEmitter);
351 |
352 | _g = new GitServer(repos.repos, logging, repoLocation, repoPort);
353 |
354 | _c = new GITCLI(_g, repos.users);
355 | |
357 |
38 | example.coffee39 | |
40 | 41 | |
44 |
45 | ¶
46 |
47 | Require the GitServer module 48 | |
49 | GitServer = require './host.js'
50 |
51 |
52 |
53 |
54 | |
56 |
59 |
71 |
60 |
68 |
61 |
66 |
67 | 62 | ¶ 63 | Create example repo 1 64 |65 |This repo does NOT allow Anonymous access 69 | |
72 |
73 | repoOne =
74 | name : 'stackdot'
75 | anonRead : false
76 | users : [
77 | user:
78 | username: 'demo'
79 | password: 'demo'
80 | permissions:['R','W']
81 | ]
82 |
83 |
84 |
85 |
86 | |
88 |
91 |
103 |
92 |
100 |
93 |
98 |
99 | 94 | ¶ 95 | Create example repo 2 96 |97 |This repo allows Anonymous reading (fetch,clone) access 101 | |
104 |
105 | repoTwo =
106 | name : 'anon'
107 | anonRead : true
108 | users : [
109 | user:
110 | username: 'demo2'
111 | password: 'demo2'
112 | permissions:['R','W']
113 | ]
114 | onSuccessful :
115 | fetch : ->
116 | console.log 'Successful fetch on "anon" repo'
117 | push : ->
118 | console.log 'Success push on "anon" repo'
119 |
120 |
121 |
122 |
123 | |
125 |
128 |
129 | ¶
130 |
131 | Put these into arrays 132 | |
133 |
134 | repos = [ repoOne, repoTwo ]
135 |
136 |
137 |
138 |
139 | |
141 |
144 |
159 |
145 |
153 |
146 |
151 |
152 | 147 | ¶ 148 | Create the GitServer object 149 |150 |We are passing in |
160 |
161 | _git = new GitServer repos, true, '/tmp/repos', 7000
162 | |
164 |
38 | example.js39 | |
40 | 41 | |
44 |
45 | ¶
46 |
47 | Generated by CoffeeScript 1.6.1 48 | |
49 | var GitServer, repoOne, repoTwo, repos, _git;
50 |
51 | GitServer = require('./host.js');
52 |
53 |
54 | |
56 |
59 |
71 |
60 |
68 |
61 |
66 |
67 | 62 | ¶ 63 | Create example repo 1 64 |65 |This repo does NOT allow Anonymous access 69 | |
72 |
73 |
74 |
75 | repoOne = {
76 | name: 'stackdot',
77 | anonRead: false,
78 | users: [
79 | {
80 | user: {
81 | username: 'demo',
82 | password: 'demo'
83 | },
84 | permissions: ['R', 'W']
85 | }
86 | ]
87 | };
88 |
89 |
90 | |
92 |
95 |
107 |
96 |
104 |
97 |
102 |
103 | 98 | ¶ 99 | Create example repo 2 100 |101 |This repo allows Anonymous reading (fetch,clone) access 105 | |
108 |
109 |
110 |
111 | repoTwo = {
112 | name: 'anon',
113 | anonRead: true,
114 | users: [
115 | {
116 | user: {
117 | username: 'demo2',
118 | password: 'demo2'
119 | },
120 | permissions: ['R', 'W']
121 | }
122 | ],
123 | onSuccessful: {
124 | fetch: function() {
125 | return console.log('Successful fetch on "anon" repo');
126 | },
127 | push: function() {
128 | return console.log('Success push on "anon" repo');
129 | }
130 | }
131 | };
132 |
133 | repos = [repoOne, repoTwo];
134 |
135 |
136 | |
138 |
141 |
156 |
142 |
150 |
143 |
148 |
149 | 144 | ¶ 145 | Create the GitServer object 146 |147 |We are passing in |
157 |
158 |
159 |
160 | _git = new GitServer(repos, true, '/tmp/repos', 7000);
161 | |
163 |
29 | host.coffee30 | |
31 | 32 | |
35 |
36 | ¶
37 |
38 |
39 |
44 | GOAL: A simple to setup and run, multi-tenant Git Server written in NodeJS. 40 |This was initially created to be used as a multi-tenant git server with powerful event triggers. 42 | |
45 |
46 | |
47 |
50 |
51 | ¶
52 |
53 | Require the modules needed: 54 | |
55 |
56 | pushover = require 'pushover'
57 | http = require 'http'
58 | https = require 'https'
59 | async = require 'async'
60 | fs = require 'fs'
61 |
62 |
63 |
64 |
65 | class GitServer
66 |
67 |
68 |
69 |
70 |
71 | |
73 |
76 |
77 | ¶
78 |
79 |
80 |
108 | Constructor function for each instance of GitServer 81 |
83 |
84 |
85 |
107 | Params
86 |
87 | repos
88 | Array
89 | List of repositories
90 |
91 |
92 | repoLocation
93 | String
94 | Location where the repo's are/will be stored
95 |
96 |
97 | port
98 | Int
99 | Port on which to run this server.
100 |
101 |
102 | certs
103 | Object
104 | Object of 'key' and 'cert' with the location of the certs (only used for HTTPS)
105 |
106 | |
109 |
110 | constructor: ( @repos = [], @logging = false, @repoLocation = '/tmp/repos', @port = 7000, @certs )->
111 |
112 |
113 | |
115 |
118 |
119 | ¶
120 |
121 | Create the pushover git object: 122 | |
123 |
124 | @git = pushover @repoLocation, autoCreate:false
125 | @permMap = fetch:'R', push:'W'
126 |
127 | |
129 |
132 |
133 | ¶
134 |
135 | Setup the repo listeners: 136 | |
137 |
138 | @gitListeners()
139 |
140 | |
142 |
145 |
146 | ¶
147 |
148 | Go through all the @repo's and create them if they dont exist: 149 | |
150 |
151 | @makeReposIfNull =>
152 |
153 | |
155 |
158 |
159 | ¶
160 |
161 | Route requests to pushover: 162 | |
163 |
164 | if @certs?
165 | @server = https.createServer @certs, @git.handle.bind(@git)
166 | else
167 | red = `'\033[31m'`
168 | reset = `'\033[0m'`
169 | message = """
170 | WARNING: No SSL certs passed in. Running as HTTP and not HTTPS.
171 | Be careful, without HTTPS your user/pass will not be encrypted"""
172 | console.log red + message + reset
173 | @server = http.createServer @git.handle.bind(@git)
174 |
175 | |
177 |
180 |
181 | ¶
182 |
183 | Open up the desired port ( 80 requires sudo ) 184 | |
185 |
186 | @server.listen @port, =>
187 |
188 | |
190 |
193 |
194 | ¶
195 |
196 | Just let the console know we have started. 197 | |
198 |
199 | @log 'Server listening on ', @port, '\r'
200 |
201 |
202 |
203 | |
205 |
208 |
209 | ¶
210 |
211 |
212 |
225 | Create a repo on the fly 213 |
215 |
216 |
217 |
224 | Params
218 |
219 | repoName
220 | Object
221 | Name of the repo we are creating.
222 |
223 | |
226 |
227 | createRepo: ( repo, callback )=>
228 | if !repo.name? or !repo.anonRead?
229 | @log 'Not enough details, need atleast .name and .anonRead'
230 | false
231 |
232 | |
234 |
237 |
238 | ¶
239 |
240 | make sure it doesnt already exist: 241 | |
242 |
243 | if !@getRepo repo.name
244 | @log 'Creating repo', repo.name
245 | @repos.push repo
246 | @git.create repo.name, callback
247 | else
248 | @log 'This repo already exists'
249 |
250 |
251 |
252 |
253 | |
255 |
258 |
259 | ¶
260 |
261 | Log all arguments passed into this function IF @logging = true 262 | |
263 |
264 | log: ()=>
265 | args = for key,value of arguments
266 | "#{value}"
267 | if @logging then console.log "LOG: ", args.join(' ')
268 |
269 |
270 |
271 |
272 |
273 | |
275 |
278 |
279 | ¶
280 |
281 |
282 |
305 | Process the request and check for basic authentication. 283 |
285 |
286 |
287 |
304 | Params
288 |
289 | gitObject
290 | Object
291 | Git object from the pushover module
292 |
293 |
294 | method
295 | String
296 | Method we are getting security for ['fetch','push']
297 |
298 |
299 | repo
300 | Object
301 | Repo object that we are doing this method on
302 |
303 | |
306 |
307 | processSecurity: ( gitObject, method, repo )=>
308 |
309 | |
311 |
314 |
315 | ¶
316 |
317 | Try to get the auth header 318 | |
319 |
320 | req = gitObject.request
321 | res = gitObject.response
322 | auth = req.headers['authorization']
323 | if auth is undefined
324 |
325 | |
327 |
330 |
331 | ¶
332 |
333 | if they didnt send a auth header, tell them we need one: 334 | |
335 |
336 | res.statusCode = 401
337 | res.setHeader 'WWW-Authenticate', 'Basic realm="Secure Area"'
338 | res.end '<html><body>Need some creds son</body></html>'
339 |
340 | |
342 |
345 |
346 | ¶
347 |
348 | now they should have responded with the auth headers: 349 | |
350 |
351 | else
352 |
353 | |
355 |
358 |
359 | ¶
360 |
361 | Decode the auth string 362 | |
363 |
364 | plain_auth = ( new Buffer( auth.split(' ')[1], 'base64' ) ).toString()
365 |
366 | |
368 |
371 |
372 | ¶
373 |
374 | split the string to get username:password 375 | |
376 |
377 | creds = plain_auth.split ':'
378 |
379 | |
381 |
384 |
385 | ¶
386 |
387 | Send off this user info and authorize it: 388 | |
389 |
390 | @permissableMethod creds[0], creds[1], method, repo, gitObject
391 |
392 |
393 |
394 |
395 |
396 | |
398 |
401 |
402 | ¶
403 |
404 |
405 |
435 | Check to see if: return 406 | Username and password match return 407 | This user has permission to do this method on this repo 408 |
410 |
411 |
412 |
434 | Params
413 |
414 | username
415 | String
416 | Username of the requesting user
417 |
418 |
419 | password
420 | String
421 | Password of the requesting user
422 |
423 |
424 | method
425 | String
426 | Method we are checking against ['fetch','push']
427 |
428 |
429 | gitObject
430 | Object
431 | Git object from pushover module
432 |
433 | |
436 |
437 | permissableMethod: ( username, password, method, repo, gitObject )=>
438 |
439 | |
441 |
444 |
445 | ¶
446 |
447 | Just let the console know someone is trying to do something that requires a password: 448 | |
449 |
450 | @log username,'is trying to', method,'on repo:',repo.name,'...'
451 |
452 | |
454 |
457 |
458 | ¶
459 |
460 | Find the user object: 461 | |
462 |
463 | user = @getUser username, password, repo
464 |
465 | |
467 |
470 |
471 | ¶
472 |
473 | check if the user exists: 474 | |
475 |
476 | if user is false
477 |
478 | |
480 |
483 |
484 | ¶
485 |
486 | This user isnt in this repo's .users array: 487 | |
488 |
489 | @log username,'was rejected as this user doesnt exist, or password is wrong'
490 | gitObject.reject(500,'Wrong username or password')
491 | else
492 | if @permMap[ method ] in user.permissions
493 | @log username,'Successfully did a', method,'on',repo.name
494 | @checkTriggers method, repo
495 | gitObject.accept()
496 | else
497 | @log username,'was rejected, no permission to',method,'on',repo.name
498 | gitObject.reject(500,"You dont have these permissions")
499 |
500 |
501 |
502 |
503 |
504 | |
506 |
509 |
510 | ¶
511 |
512 | Setup the listeners for git events: 513 | |
514 |
515 | gitListeners: ()=>
516 |
517 | |
519 |
522 |
523 | ¶
524 |
525 | On each git push request 526 | |
527 |
528 | @git.on 'push', @onPush
529 |
530 | |
532 |
535 |
536 | ¶
537 |
538 | On each git fetch request 539 | |
540 |
541 | @git.on 'fetch', @onFetch
542 |
543 | |
545 |
548 |
549 | ¶
550 |
551 | On each git info request 552 | |
553 |
554 | @git.on 'info', @onFetch
555 |
556 |
557 |
558 |
559 |
560 | |
562 |
565 |
566 | ¶
567 |
568 |
569 |
576 | Checks all the passed in repo's to make sure they all have a real .git directory. 570 |
572 |
573 |
574 |
575 | |
577 |
578 | makeReposIfNull: ( callback )=>
579 | @log 'Making repos if they dont exist';
580 |
581 | |
583 |
586 |
587 | ¶
588 |
589 | Get all the repo names in an Array 590 | |
591 |
592 | repoNames = []
593 | for repo in @repos
594 |
595 | |
597 |
600 |
601 | ¶
602 |
603 | Make sure this repo has the require fields, if so, add to array: 604 | |
605 |
606 | if repo.name? and repo.anonRead? and repo.users?
607 | repoNames.push("#{repo.name}.git")
608 |
609 | |
611 |
614 |
615 | ¶
616 |
617 | This repo was missing some field we require 618 | |
619 |
620 | else
621 | console.log 'Bad Repo', repo.name, 'is missing an attribute..'
622 |
623 | |
625 |
628 |
629 | ¶
630 |
631 | Call .exists on each repo name 632 | |
633 |
634 | async.reject repoNames, @git.exists.bind(@git), ( results )=>
635 |
636 | |
638 |
641 |
642 | ¶
643 |
644 | If we have repo's that need to be created: 645 | |
646 |
647 | if results.length > 0
648 |
649 | |
651 |
654 |
655 | ¶
656 |
657 | Create each repo that doesn not exist: 658 | |
659 |
660 | console.log('Creating repo directory: ', repo ) for repo in results
661 |
662 | |
664 |
667 |
668 | ¶
669 |
670 | call .create on each repo: 671 | |
672 |
673 | async.map results, @git.create.bind(@git), callback
674 | else callback() # Otherwise, open up the server.
675 |
676 |
677 |
678 |
679 |
680 | |
682 |
685 |
686 | ¶
687 |
688 |
689 |
702 | When the git fetch command is triggered, this is fired. 690 |
692 |
693 |
694 |
701 | Params
695 |
696 | fetch
697 | Object
698 | Git object from pushover module.
699 |
700 | |
703 |
704 | onFetch: ( fetch )=>
705 | @log 'Got a FETCH call for', fetch.repo
706 | repo = @getRepo fetch.repo
707 | if repo isnt false # if this repo actually exists:
708 |
709 | |
711 |
714 |
715 | ¶
716 |
717 | This repo allows anyone to fetch it, so accept the request: 718 | |
719 |
720 | if repo.anonRead is true
721 | @checkTriggers 'fetch', repo
722 | fetch.accept()
723 |
724 | |
726 |
729 |
730 | ¶
731 |
732 | this repo has no anon access, so we need to check the user/pass 733 | |
734 |
735 | else
736 | @processSecurity fetch, 'fetch', repo
737 | else # otherwise we need to reject this
738 | @log 'Rejected - Repo',fetch.repo,'doesnt exist'
739 | fetch.reject(500,'This repo doesnt exist')
740 |
741 |
742 |
743 |
744 |
745 | |
747 |
750 |
751 | ¶
752 |
753 |
754 |
767 | When the git push command is triggered, this is fired. 755 |
757 |
758 |
759 |
766 | Params
760 |
761 | push
762 | Object
763 | Git object from pushover module.
764 |
765 | |
768 |
769 | onPush: ( push )=>
770 | @log 'Got a PUSH call for', push.repo
771 | repo = @getRepo push.repo
772 | if repo isnt false # if this repo actually exists:
773 | @processSecurity push, 'push', repo
774 | else
775 | @log 'Rejected - Repo',push.repo,'doesnt exist'
776 | push.reject(500,'This repo doesnt exist')
777 |
778 |
779 |
780 |
781 |
782 | |
784 |
787 |
788 | ¶
789 |
790 |
791 |
809 | Check if this repo has onSuccessful triggers 792 |
794 |
795 |
796 |
808 | Params
797 |
798 | method
799 | String
800 | fetch|push
801 |
802 |
803 | repo
804 | Object
805 | Repo object we are checking
806 |
807 | |
810 |
811 | checkTriggers: ( method, repo )=>
812 |
813 | |
815 |
818 |
819 | ¶
820 |
821 | If .onSuccessful exists: 822 | |
823 |
824 | if repo.onSuccessful?
825 |
826 | |
828 |
831 |
832 | ¶
833 |
834 | If this method exists in it: 835 | |
836 |
837 | if repo.onSuccessful[method]?
838 |
839 | |
841 |
844 |
845 | ¶
846 |
847 | log it, and call it 848 | |
849 |
850 | @log 'On successful triggered: ', method, 'on',repo.name
851 | repo.onSuccessful[method]?( repo, method )
852 |
853 |
854 |
855 |
856 |
857 | |
859 |
862 |
863 | ¶
864 |
865 |
866 |
889 | Get the user object, check user/pass is correct and it exists in this repo. 867 |
869 |
870 |
871 |
888 | Params
872 |
873 | username
874 | String
875 | Username to find
876 |
877 |
878 | password
879 | String
880 | Password of the Username
881 |
882 |
883 | repo
884 | Object
885 | Repo object this user should be in.
886 |
887 | |
890 |
891 | getUser: ( username, password, repo )=>
892 | for userObject in repo.users
893 |
894 | |
896 |
899 |
900 | ¶
901 |
902 | If we found this user, return it 903 | |
904 |
905 | return userObject if userObject.user.username is username and userObject.user.password is password
906 | false # Otherwise, return a false
907 |
908 |
909 |
910 |
911 |
912 | |
914 |
917 |
918 | ¶
919 |
920 |
921 |
934 | Get the repo from the array of repos 922 |
924 |
925 |
926 |
933 | Params
927 |
928 | repoName
929 | String
930 | Name of the repo we are trying to find
931 |
932 | |
935 |
936 | getRepo: ( repoName )=>
937 | for repo in @repos
938 |
939 | |
941 |
944 |
945 | ¶
946 |
947 | If the repo exists, return it. 948 | |
949 |
950 | return repo if repo.name+'.git' is repoName
951 | false # Otherwise, return a false
952 |
953 |
954 |
955 |
956 | |
958 |
961 |
962 | ¶
963 |
964 | Export this as a module: 965 | |
966 |
967 | module.exports = GitServer
968 | |
970 |