Version api trong Golang

Dạo này tôi hay viết dịch vụ với golang, chỉ một vài cái thôi, nhưng cũng đủ để làm tôi quên mất là production/staging đang chạy version nào.

Với nodejs chẳng hạn, thì tôi sẽ export version từ package.json ra một api nào đấy.

Với golang thì không có thứ tựa như package.json. Và thật sự cũng không cần thiết, vì những thông tin này có thể đưa vào binary file khi build.

Ví dụ tôi quan tâm đến là build timestamp, githash và gittag. Lấy trực tiếp từ shell
– timestamp: date -u '+%Y-%m-%d_%I:%M:%S%p'
(e.g. 2018-10-13_08:26:34AM)
– githash: git rev-parse HEAD
– gittag: git describe --tags $(git rev-list --tags --max-count=1)

Vấn đề tiếp theo là làm thế nào để đưa các thông tin này vào lúc tạo artifact? Từ một issue của golang https://github.com/golang/go/issues/18246, (yay, golang cần doc tốt hơn) cách thức là

-ldflags “-X importpath.name=value”

Cuối cùng, cờ của tôi sẽ có giá trị như sau

LDFLAGS=”-X gitlab.com/nqd/example/handlers.buildstamp=date -u '+%Y-%m-%d_%I:%M:%S%p' -X gitlab.com/nqd/example/handlers.githash=git rev-parse HEAD -X gitlab.com/nqd/example/handlers.gittag=git describe --tags $(git rev-list --tags --max-count=1)

Viết gọn lại

timestamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'`
githash=`git rev-parse HEAD`
gittag=`git describe --tags $(git rev-list --tags --max-count=1)`
LDFLAGS="-X gitlab.com/nqd/example/handlers.buildstamp=$timestamp -X gitlab.com/nqd/example/handlers.githash=$githash -X gitlab.com/nqd/example/handlers.gittag=$gittag"

Cờ này bảo rằng tôi sẽ thay thế biến buildstamp, githash, và gittag bằng những giá trị từ shell.
Dịch project với cờ thêm vào này

$(GO) build -ldflags $(LDFLAGS) ...

Ở handlers.go (gitlab.com/nqd/example/handler.go), nơi chứa các biến buildstamp, githash, gittag, có thể xuất ra một path nào đấy:

var buildstamp = "no buildstamp provided"
var githash = "no githash provided"
var gittag = "no tag"

type statuser struct {
  Build string `json:"build"`
  Time string `json:"time"`
  Version string `json:"version"`
}

func status(w http.ResponseWriter, r *http.Request) {
  st := statuser{
    Build: githash,
    Time: buildstamp,
    Version: gittag,
  }
  render.Render(w, r, &st)
}
Với cùng cách thức, tôi có thể đưa các giá trị bất kỳ vào binary trong khi CI build artifact.

 

make some part of api security checklist happy: output

Continue to meet some requirement in the api security checklist: output with loopback/express.

Some recommendations:

  • Send X-Content-Type-Options: nosniff header.
  • Send X-Frame-Options: deny header.
  • Send Content-Security-Policy: default-src ‘none’ header.
  • Remove fingerprinting headers – X-Powered-By, Server, X-AspNet-Version etc.

In nodejs territory, with loopback/express area, we can control the header with helmet.

What’s helmetjs doing? It controls https header which help us meet some requirements above.

Lets do it with this loopback middleware setting.

{
  "initial": {
  ...
    "helmet-csp": {
      "params": {
        "directives": {
          "defaultSrc": [
            "\"none\""
          ]
        }
      }
    },
    "helmet#xssFilter": {},
    "helmet#frameguard": {
      "params": {
        "action": "deny"
      }
    },
    "helmet#hsts": {
      "params": {
        "maxAge": 0,
        "includeSubdomains": true
      }
    },
    "helmet#hidePoweredBy": {},
    "helmet#ieNoOpen": {},
    "helmet#noSniff": {},
    "helmet#noCache": {
      "enabled": false
    },
  ...
  }

For Content-Security-Policy setting, helmet dont enable it by default, so I will need to install another package helmet-csp, which explains why the middleware setting has item helmet-csp instead of helmet#... like the others.

Cool, lets double check it. You can use curl to verify, but I love everything to be automatic. Lets do it with mocha and suppertest.

const request = require('supertest')
const app = require('../../server/server')
const faker = require('faker')
const expect = require('chai').expect

function req (verb, url) {
  return request(app)[verb](url)
    .set('Content-Type', 'application/json')
    .set('Accept', 'application/json')
    .expect('Content-Type', /json/)
}

describe('HTTP header', () => {
  describe('should send login request, and returned headers', () => {
    let headers
    before(done => {
      req('post', '/api/users/login')
        .send({ email: faker.internet.email(), password: faker.internet.password() })
        .expect(401, function (err, res) {
          expect(err).to.be.null
          headers = res.header
          done()
        })
    })
    it('Content-Security-Policy: default-src none header', done => {
      expect(headers['content-security-policy']).to.be.eq('default-src "none"')
      done()
    })
    it('X-Content-Type-Options: nosniff', done => {
      expect(headers['x-content-type-options']).to.be.eq('nosniff')
      done()
    })
    it('X-Frame-Options: deny', done => {
      expect(headers['x-frame-options']).to.be.eq('DENY')
      done()
    })
    it('remove header X-Powered-By, Server', done => {
      expect(headers['x-powered-by']).to.be.null
      expect(headers['server']).to.be.null
      done()
    })
  })
})

Actually the middleware.json setting doing more than the API-Security-Checklist list on http header output. Read more at Loopback block on security practice

loopback login rate limit

Problem: limit number of login for one user.

Usually user will login with this api

POST /api/users/login
{"email": "foo@bar.baz", "password":"what is a password"}

Statement: Given this user, how could I limit login number n within i interval.

There is a great module from https://github.com/chriso/redback, that provide rate limit https://gist.github.com/chriso/54dd46b03155fcf555adccea822193da. We can use this idea to limit number of login for one user

{ratelimit/login.js}
const redis = require('redis').createClient(REDIS_URL)
const redback = require('redback').use(redis)

let ratelimit = redback.createRateLimit('login')

module.exports = function (req, res, next) {
  const loginRetryInterval = 60 * 5   // 5 mins
  const loginRetryMax = 5             // 5 times

  const key = `body.email:${req.body.email}`
  ratelimit.add(key)

  // count the number of requests in the last 20 seconds
  ratelimit.count(key, loginRetryInterval, (err, requests) => {
    if (err) { return next() }

    if (requests >= loginRetryMax) {
      return res.type('json').status(429).end()
    }

    return next()
  })
}

I will use redis instead of local memory because the program may run in multiple workers.

For loopback, I may use this before loopback itself middleware. At server.js

app.use('/api/users/login', require('./ratelimit/login'))

Using email address as only key to measure attack is so naive, since the true owner of the account cannot login because someone was trying to guess the password? I should include source IP address into the key.

...
const key = `connection.remoteAddress:${req.connection.remoteAddress}:body.email:${req.body.email}`
ratelimit.add(key)
...

With this define, redis will have key like "login:connection.remoteAddress:::ffff:127.0.0.1:body.email:foo@bar.baz".

Cheer.