All files / node-git-server/lib service.js

87.04% Statements 94/108
71.05% Branches 27/38
77.78% Functions 14/18
88.35% Lines 91/103

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 2122x 2x 2x 2x 2x   2x   2x         2x 6x 6x                         11x   11x 11x   11x 11x 11x 11x 11x   11x     11x   11x         11x         11x     11x               11x 11x   11x 11x 11x   11x   13x   13x 9x 9x   9x 7x 7x   2x 2x     9x       9x 9x 4x 4x 4x 4x             11x 10x 10x         10x   10x       10x   10x 24x 22x 1x 3x       22x     2x 1x   10x   9x     10x 3x     10x         10x   10x 10x   10x 10x 1x 3x   1x 1x     10x         11x 1x 1x         6x 6x 6x 6x   6x                   1x   1x       1x 1x               12x   10x 10x       2x  
const through = require('through');
const zlib = require('zlib');
const util = require('util');
const os = require('os');
const { spawn } = require('child_process');
 
const HttpDuplex = require('./http-duplex');
 
const headerRE = {
  'receive-pack': '([0-9a-fA-F]+) ([0-9a-fA-F]+) refs\/(heads|tags)\/(.*?)( |00|\u0000)|^(0000)$', // eslint-disable-line
  'upload-pack': '^\\S+ ([0-9a-fA-F]+)'
};
 
const packSideband = s => {
  const n = (4 + s.length).toString(16);
  return Array(4 - n.length + 1).join('0') + n + s;
};
 
class Service extends HttpDuplex {
  /**
   * Handles invoking the git-*-pack binaries
   * @class Service
   * @extends HttpDuplex
   * @param  {Object}               opts - options to bootstrap the service object
   * @param  {http.IncomingMessage }   req  - http request object
   * @param  {http.ServerResponse}  res  - http response
   */
  constructor(opts, req, res) {
    super(req, res);
 
    var data = '';
    var self = this;
 
    this.status = 'pending';
    this.repo = opts.repo;
    this.service = opts.service;
    this.cwd = opts.cwd;
    this.logs = [];
 
    var buffered = through().pause();
 
    // stream needed to receive data after decoding, but before accepting
    var ts = through();
 
    var decoder = {
        'gzip': () => zlib.createGunzip(),
        'deflate': () => zlib.createDeflate()
    }[req.headers['content-encoding']];
 
    Iif (decoder) {
        // data is compressed with gzip or deflate
        req.pipe(decoder()).pipe(ts).pipe(buffered);
    } else {
        // data is not compressed
        req.pipe(ts).pipe(buffered);
    }
 
    Iif(req.headers["authorization"]) {
      const tokens = req.headers["authorization"].split(" ");
      if (tokens[0] === "Basic") {
          const splitHash = Buffer.from(tokens[1], 'base64').toString('utf8').split(":");
          this.username = splitHash.shift();
      }
    }
 
    ts.once('data', function onData(buf) {
        data += buf;
 
        var ops = data.match(new RegExp(headerRE[self.service], 'gi'));
        Iif (!ops) return;
        data = undefined;
 
        ops.forEach(function(op) {
            var type;
            var m = op.match(new RegExp(headerRE[self.service]));
 
            if (self.service === 'receive-pack') {
                self.last = m[1];
                self.commit = m[2];
 
                if (m[3] == 'heads') {
                    type = 'branch';
                    self.evName = 'push';
                } else {
                    type = 'version';
                    self.evName = 'tag';
                }
 
                var headers = {
                    last: self.last,
                    commit: self.commit
                };
                headers[type] = self[type] = m[4];
                self.emit('header', headers);
            } else Eif (self.service === 'upload-pack') {
                self.commit = m[1];
                self.evName = 'fetch';
                self.emit('header', {
                    commit: self.commit
                });
            }
        });
    });
 
    self.once('accept', function onAccept() {
        process.nextTick(function() {
            const cmd = os.platform() == 'win32' ?
              ['git', opts.service, '--stateless-rpc', opts.cwd]
            :
              ['git-' + opts.service, '--stateless-rpc', opts.cwd];
 
            const ps = spawn(cmd[0], cmd.slice(1));
 
            ps.on('error', function(err) {
              self.emit('error', new Error(`${err.message} running command ${cmd.join(' ')}`));
            });
 
            self.emit('service', ps);
 
            var respStream = through(function write(c) {
                if (self.listeners('response').length === 0) {
                  if(self.logs.length > 0) {
                    while(self.logs.length > 0) {
                      this.queue(self.logs.pop());
                    }
                  }
 
                  return this.queue(c);
                }
                // prevent git from sending the close signal
                if (c.length === 4 && c.toString() === '0000') return;
                this.queue(c);
            }, function end() {
                if (self.listeners('response').length > 0) return;
 
                this.queue(null);
            });
 
            respStream.log = function() {
              self.log(...arguments);
            };
 
            self.emit('response', respStream, function endResponse() {
                res.queue(Buffer.from('0000'));
                res.queue(null);
            });
 
            ps.stdout.pipe(respStream).pipe(res);
 
            buffered.pipe(ps.stdin);
            buffered.resume();
 
            ps.on('exit', () => {
              if(self.logs.length > 0) {
                while(self.logs.length > 0) {
                  respStream.queue(self.logs.pop());
                }
                respStream.queue(Buffer.from('0000'));
                respStream.queue(null);
              }
 
              self.emit.bind(self, 'exit');
            });
        });
    });
 
    self.once('reject', function onReject(code, msg) {
        res.statusCode = code;
        res.end(msg);
    });
  }
 
  log() {
    const _log = util.format(...arguments);
    const SIDEBAND = String.fromCharCode(2); // PROGRESS
    const message = `${SIDEBAND}${_log}\n`;
    const formattedMessage = Buffer.from(packSideband(message));
 
    this.logs.unshift(formattedMessage);
  }
  /**
   * reject request in flight
   * @method reject
   * @memberof Service
   * @param  {Number} code - http response code
   * @param  {String} msg  - message that should be displayed on teh client
   */
  reject(code, msg) {
      Iif (this.status !== 'pending') return;
 
      Iif (msg === undefined && typeof code === 'string') {
          msg = code;
          code = 500;
      }
      this.status = 'rejected';
      this.emit('reject', code || 500, msg);
  }
  /**
   * accepts request to access resource
   * @method accept
   * @memberof Service
   */
  accept() {
      if (this.status !== 'pending') return;
 
      this.status = 'accepted';
      this.emit('accept');
  }
}
 
module.exports = Service;