host.coffee

GOAL: A simple to setup and run, multi-tenant Git Server written in NodeJS.

This was initially created to be used as a multi-tenant git server with powerful event triggers.

Require the modules needed:

pushover  = require 'pushover'
http    = require 'http'
https   = require 'https'
async   = require 'async'
fs      = require 'fs'




class GitServer
  
  
  
  

Constructor function for each instance of GitServer

Params
repos Array List of repositories
repoLocation String Location where the repo's are/will be stored
port Int Port on which to run this server.
certs Object Object of 'key' and 'cert' with the location of the certs (only used for HTTPS)
  constructor: ( @repos = [], @logging = false, @repoLocation = '/tmp/repos', @port = 7000, @certs )->
    

Create the pushover git object:

    @git    = pushover @repoLocation, autoCreate:false
    @permMap  = fetch:'R', push:'W'

Setup the repo listeners:

    @gitListeners()

Go through all the @repo's and create them if they dont exist:

    @makeReposIfNull =>

Route requests to pushover:

      if @certs?
        @server = https.createServer @certs, @git.handle.bind(@git)
      else
        red   = `'\033[31m'`
        reset = `'\033[0m'`
        message = """
          WARNING: No SSL certs passed in. Running as HTTP and not HTTPS.
          Be careful, without HTTPS your user/pass will not be encrypted"""
        console.log red + message + reset
        @server = http.createServer @git.handle.bind(@git)

Open up the desired port ( 80 requires sudo )

      @server.listen @port, =>

Just let the console know we have started.

        @log 'Server listening on ', @port, '\r'
  
  

Create a repo on the fly

Params
repoName Object Name of the repo we are creating.
  createRepo: ( repo, callback )=>
    if !repo.name? or !repo.anonRead?
      @log 'Not enough details, need atleast .name and .anonRead'
      false

make sure it doesnt already exist:

    if !@getRepo repo.name
      @log 'Creating repo', repo.name
      @repos.push repo
      @git.create repo.name, callback
    else
      @log 'This repo already exists'
  
  
  

Log all arguments passed into this function IF @logging = true

  log: ()=>
    args = for key,value of arguments
      "#{value}"
    if @logging then console.log "LOG: ", args.join(' ')
  
  
  
  

Process the request and check for basic authentication.

Params
gitObject Object Git object from the pushover module
method String Method we are getting security for ['fetch','push']
repo Object Repo object that we are doing this method on
  processSecurity: ( gitObject, method, repo )=>

Try to get the auth header

    req   = gitObject.request
    res   = gitObject.response
    auth  = req.headers['authorization']
    if auth is undefined

if they didnt send a auth header, tell them we need one:

      res.statusCode = 401
      res.setHeader 'WWW-Authenticate', 'Basic realm="Secure Area"'
      res.end '<html><body>Need some creds son</body></html>'

now they should have responded with the auth headers:

    else

Decode the auth string

      plain_auth  = ( new Buffer( auth.split(' ')[1], 'base64' ) ).toString()

split the string to get username:password

      creds = plain_auth.split ':'

Send off this user info and authorize it:

      @permissableMethod creds[0], creds[1], method, repo, gitObject
  
  
  
  

Check to see if: return Username and password match return This user has permission to do this method on this repo

Params
username String Username of the requesting user
password String Password of the requesting user
method String Method we are checking against ['fetch','push']
gitObject Object Git object from pushover module
  permissableMethod: ( username, password, method, repo, gitObject )=>

Just let the console know someone is trying to do something that requires a password:

    @log username,'is trying to', method,'on repo:',repo.name,'...'

Find the user object:

    user = @getUser username, password, repo

check if the user exists:

    if user is false

This user isnt in this repo's .users array:

      @log username,'was rejected as this user doesnt exist, or password is wrong'
      gitObject.reject(500,'Wrong username or password')
    else
      if @permMap[ method ] in user.permissions
        @log username,'Successfully did a', method,'on',repo.name
        gitObject.accept()
      else
        @log username,'was rejected, no permission to',method,'on',repo.name
        gitObject.reject(500,"You dont have these permissions")
  
  
  
  

Setup the listeners for git events:

  gitListeners: ()=>

On each git push request

    @git.on 'push', @onPush

On each git fetch request

    @git.on 'fetch', @onFetch

On each git info request

    @git.on 'info', @onFetch
  
  
  
  

Checks all the passed in repo's to make sure they all have a real .git directory.

  makeReposIfNull: ( callback )=>
    @log 'Making repos if they dont exist';

Get all the repo names in an Array

    repoNames = []
    for repo in @repos

Make sure this repo has the require fields, if so, add to array:

      if repo.name? and repo.anonRead? and repo.users?
        repoNames.push("#{repo.name}.git")

This repo was missing some field we require

      else
        console.log 'Bad Repo', repo.name, 'is missing an attribute..'

Call .exists on each repo name

    async.reject repoNames, @git.exists.bind(@git), ( results )=>

If we have repo's that need to be created:

      if results.length > 0

Create each repo that doesn not exist:

        console.log('Creating repo directory: ', repo ) for repo in results

call .create on each repo:

        async.map results, @git.create.bind(@git), callback
      else callback() # Otherwise, open up the server.
  
  
  
  

When the git fetch command is triggered, this is fired.

Params
fetch Object Git object from pushover module.
  onFetch: ( fetch )=>
    @log 'Got a FETCH call for', fetch.repo
    repo = @getRepo fetch.repo
    if repo isnt false # if this repo actually exists:

This repo allows anyone to fetch it, so accept the request:

      if repo.anonRead is true
        fetch.accept()

this repo has no anon access, so we need to check the user/pass

      else
        @processSecurity fetch, 'fetch', repo
    else # otherwise we need to reject this
      @log 'Rejected - Repo',fetch.repo,'doesnt exist'
      fetch.reject(500,'This repo doesnt exist')
  
  
  
  

When the git push command is triggered, this is fired.

Params
push Object Git object from pushover module.
  onPush: ( push )=>
    @log 'Got a PUSH call for', push.repo
    repo = @getRepo push.repo
    if repo isnt false # if this repo actually exists:
      @processSecurity push, 'push', repo
    else
      @log 'Rejected - Repo',push.repo,'doesnt exist'
      push.reject(500,'This repo doesnt exist')
  
  
  
  

Get the user object, check user/pass is correct and it exists in this repo.

Params
username String Username to find
password String Password of the Username
repo Object Repo object this user should be in.
  getUser: ( username, password, repo )=>
    for userObject in repo.users

If we found this user, return it

      return userObject if userObject.user.username is username and userObject.user.password is password
    false # Otherwise, return a false
  
  
  
  

Get the repo from the array of repos

Params
repoName String Name of the repo we are trying to find
  getRepo: ( repoName )=>
    for repo in @repos

If the repo exists, return it.

      return repo if repo.name+'.git' is repoName
    false # Otherwise, return a false




Export this as a module:

module.exports = GitServer