Add MFA functionality

This commit is contained in:
JoramWilander 2016-03-30 12:49:29 -04:00
parent 2aa0d9b8fc
commit f9a3a4b394
49 changed files with 5845 additions and 361 deletions

12
Godeps/Godeps.json generated
View file

@ -22,6 +22,10 @@
"ImportPath": "github.com/cloudfoundry/jibber_jabber",
"Rev": "bcc4c8345a21301bf47c032ff42dd1aae2fe3027"
},
{
"ImportPath": "github.com/dgryski/dgoogauth",
"Rev": "67642ac6f9144f6610279e37e7be9af13f1cd668"
},
{
"ImportPath": "github.com/disintegration/imaging",
"Rev": "546cb3c5137b3f1232e123a26aa033aade6b3066"
@ -98,6 +102,14 @@
"Comment": "go1.0-cutoff-63-g11fc39a",
"Rev": "11fc39a580a008f1f39bb3d11d984fb34ed778d9"
},
{
"ImportPath": "github.com/mattermost/rsc/gf256",
"Rev": "bbaefb05eaa0389ea712340066837c8ce4d287f9"
},
{
"ImportPath": "github.com/mattermost/rsc/qr",
"Rev": "bbaefb05eaa0389ea712340066837c8ce4d287f9"
},
{
"ImportPath": "github.com/mssola/user_agent",
"Comment": "v0.4.1-5-g783ec61",

View file

@ -0,0 +1 @@
language: go

View file

@ -0,0 +1,15 @@
This is a Go implementation of the Google Authenticator library.
[![Build Status](https://travis-ci.org/dgryski/dgoogauth.png)](https://travis-ci.org/dgryski/dgoogauth)
Copyright (c) 2012 Damian Gryski <damian@gryski.com>
This code is licensed under the Apache License, version 2.0
It implements the one-time-password algorithms specified in:
* RFC 4226 (HOTP: An HMAC-Based One-Time Password Algorithm)
* RFC 6238 (TOTP: Time-Based One-Time Password Algorithm)
You can learn more about the Google Authenticator library at its project page:
* https://github.com/google/google-authenticator

View file

@ -0,0 +1,199 @@
/*
Package dgoogauth implements the one-time password algorithms supported by Google Authenticator
This package supports the HMAC-Based One-time Password (HOTP) algorithm
specified in RFC 4226 and the Time-based One-time Password (TOTP) algorithm
specified in RFC 6238.
*/
package dgoogauth
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base32"
"encoding/binary"
"errors"
"net/url"
"sort"
"strconv"
"time"
)
// Much of this code assumes int == int64, which probably is not the case.
// ComputeCode computes the response code for a 64-bit challenge 'value' using the secret 'secret'.
// To avoid breaking compatibility with the previous API, it returns an invalid code (-1) when an error occurs,
// but does not silently ignore them (it forces a mismatch so the code will be rejected).
func ComputeCode(secret string, value int64) int {
key, err := base32.StdEncoding.DecodeString(secret)
if err != nil {
return -1
}
hash := hmac.New(sha1.New, key)
err = binary.Write(hash, binary.BigEndian, value)
if err != nil {
return -1
}
h := hash.Sum(nil)
offset := h[19] & 0x0f
truncated := binary.BigEndian.Uint32(h[offset : offset+4])
truncated &= 0x7fffffff
code := truncated % 1000000
return int(code)
}
// ErrInvalidCode indicate the supplied one-time code was not valid
var ErrInvalidCode = errors.New("invalid code")
// OTPConfig is a one-time-password configuration. This object will be modified by calls to
// Authenticate and should be saved to ensure the codes are in fact only used
// once.
type OTPConfig struct {
Secret string // 80-bit base32 encoded string of the user's secret
WindowSize int // valid range: technically 0..100 or so, but beyond 3-5 is probably bad security
HotpCounter int // the current otp counter. 0 if the user uses time-based codes instead.
DisallowReuse []int // timestamps in the current window unavailable for re-use
ScratchCodes []int // an array of 8-digit numeric codes that can be used to log in
UTC bool // use UTC for the timestamp instead of local time
}
func (c *OTPConfig) checkScratchCodes(code int) bool {
for i, v := range c.ScratchCodes {
if code == v {
// remove this code from the list of valid ones
l := len(c.ScratchCodes) - 1
c.ScratchCodes[i] = c.ScratchCodes[l] // copy last element over this element
c.ScratchCodes = c.ScratchCodes[0:l] // and trim the list length by 1
return true
}
}
return false
}
func (c *OTPConfig) checkHotpCode(code int) bool {
for i := 0; i < c.WindowSize; i++ {
if ComputeCode(c.Secret, int64(c.HotpCounter+i)) == code {
c.HotpCounter += i + 1
// We don't check for overflow here, which means you can only authenticate 2^63 times
// After that, the counter is negative and the above 'if' test will fail.
// This matches the behaviour of the PAM module.
return true
}
}
// we must always advance the counter if we tried to authenticate with it
c.HotpCounter++
return false
}
func (c *OTPConfig) checkTotpCode(t0, code int) bool {
minT := t0 - (c.WindowSize / 2)
maxT := t0 + (c.WindowSize / 2)
for t := minT; t <= maxT; t++ {
if ComputeCode(c.Secret, int64(t)) == code {
if c.DisallowReuse != nil {
for _, timeCode := range c.DisallowReuse {
if timeCode == t {
return false
}
}
// code hasn't been used before
c.DisallowReuse = append(c.DisallowReuse, t)
// remove all time codes outside of the valid window
sort.Ints(c.DisallowReuse)
min := 0
for c.DisallowReuse[min] < minT {
min++
}
// FIXME: check we don't have an off-by-one error here
c.DisallowReuse = c.DisallowReuse[min:]
}
return true
}
}
return false
}
// Authenticate a one-time-password against the given OTPConfig
// Returns true/false if the authentication was successful.
// Returns error if the password is incorrectly formatted (not a zero-padded 6 or non-zero-padded 8 digit number).
func (c *OTPConfig) Authenticate(password string) (bool, error) {
var scratch bool
switch {
case len(password) == 6 && password[0] >= '0' && password[0] <= '9':
break
case len(password) == 8 && password[0] >= '1' && password[0] <= '9':
scratch = true
break
default:
return false, ErrInvalidCode
}
code, err := strconv.Atoi(password)
if err != nil {
return false, ErrInvalidCode
}
if scratch {
return c.checkScratchCodes(code), nil
}
// we have a counter value we can use
if c.HotpCounter > 0 {
return c.checkHotpCode(code), nil
}
var t0 int
// assume we're on Time-based OTP
if c.UTC {
t0 = int(time.Now().UTC().Unix() / 30)
} else {
t0 = int(time.Now().Unix() / 30)
}
return c.checkTotpCode(t0, code), nil
}
// ProvisionURI generates a URI that can be turned into a QR code to configure
// a Google Authenticator mobile app.
func (c *OTPConfig) ProvisionURI(user string) string {
return c.ProvisionURIWithIssuer(user, "")
}
// ProvisionURIWithIssuer generates a URI that can be turned into a QR code
// to configure a Google Authenticator mobile app. It respects the recommendations
// on how to avoid conflicting accounts.
//
// See https://code.google.com/p/google-authenticator/wiki/ConflictingAccounts
func (c *OTPConfig) ProvisionURIWithIssuer(user string, issuer string) string {
auth := "totp/"
q := make(url.Values)
if c.HotpCounter > 0 {
auth = "hotp/"
q.Add("counter", strconv.Itoa(c.HotpCounter))
}
q.Add("secret", c.Secret)
if issuer != "" {
q.Add("issuer", issuer)
auth += issuer + ":"
}
return "otpauth://" + auth + user + "?" + q.Encode()
}

View file

@ -0,0 +1,251 @@
package dgoogauth
import (
"strconv"
"testing"
"time"
)
// Test vectors via:
// http://code.google.com/p/google-authenticator/source/browse/libpam/pam_google_authenticator_unittest.c
// https://google-authenticator.googlecode.com/hg/libpam/totp.html
var codeTests = []struct {
secret string
value int64
code int
}{
{"2SH3V3GDW7ZNMGYE", 1, 293240},
{"2SH3V3GDW7ZNMGYE", 5, 932068},
{"2SH3V3GDW7ZNMGYE", 10000, 50548},
}
func TestCode(t *testing.T) {
for _, v := range codeTests {
c := ComputeCode(v.secret, v.value)
if c != v.code {
t.Errorf("computeCode(%s, %d): got %d expected %d\n", v.secret, v.value, c, v.code)
}
}
}
func TestScratchCode(t *testing.T) {
var cotp OTPConfig
cotp.ScratchCodes = []int{11112222, 22223333}
var scratchTests = []struct {
code int
result bool
}{
{33334444, false},
{11112222, true},
{11112222, false},
{22223333, true},
{22223333, false},
{33334444, false},
}
for _, s := range scratchTests {
r := cotp.checkScratchCodes(s.code)
if r != s.result {
t.Errorf("scratchcode(%d) failed: got %t expected %t", s.code, r, s.result)
}
}
}
func TestHotpCode(t *testing.T) {
var cotp OTPConfig
// reuse our test values from above
// perhaps create more?
cotp.Secret = "2SH3V3GDW7ZNMGYE"
cotp.HotpCounter = 1
cotp.WindowSize = 3
var counterCodes = []struct {
code int
result bool
counter int
}{
{ /* 1 */ 293240, true, 2}, // increments on success
{ /* 1 */ 293240, false, 3}, // and failure
{ /* 5 */ 932068, true, 6}, // inside of window
{ /* 10 */ 481725, false, 7}, // outside of window
{ /* 10 */ 481725, false, 8}, // outside of window
{ /* 10 */ 481725, true, 11}, // now inside of window
}
for i, s := range counterCodes {
r := cotp.checkHotpCode(s.code)
if r != s.result {
t.Errorf("counterCode(%d) (step %d) failed: got %t expected %t", s.code, i, r, s.result)
}
if cotp.HotpCounter != s.counter {
t.Errorf("hotpCounter incremented poorly: got %d expected %d", cotp.HotpCounter, s.counter)
}
}
}
func TestTotpCode(t *testing.T) {
var cotp OTPConfig
// reuse our test values from above
cotp.Secret = "2SH3V3GDW7ZNMGYE"
cotp.WindowSize = 5
var windowTest = []struct {
code int
t0 int
result bool
}{
{50548, 9997, false},
{50548, 9998, true},
{50548, 9999, true},
{50548, 10000, true},
{50548, 10001, true},
{50548, 10002, true},
{50548, 10003, false},
}
for i, s := range windowTest {
r := cotp.checkTotpCode(s.t0, s.code)
if r != s.result {
t.Errorf("counterCode(%d) (step %d) failed: got %t expected %t", s.code, i, r, s.result)
}
}
cotp.DisallowReuse = make([]int, 0)
var noreuseTest = []struct {
code int
t0 int
result bool
disallowed []int
}{
{50548 /* 10000 */, 9997, false, []int{}},
{50548 /* 10000 */, 9998, true, []int{10000}},
{50548 /* 10000 */, 9999, false, []int{10000}},
{478726 /* 10001 */, 10001, true, []int{10000, 10001}},
{646986 /* 10002 */, 10002, true, []int{10000, 10001, 10002}},
{842639 /* 10003 */, 10003, true, []int{10001, 10002, 10003}},
}
for i, s := range noreuseTest {
r := cotp.checkTotpCode(s.t0, s.code)
if r != s.result {
t.Errorf("timeCode(%d) (step %d) failed: got %t expected %t", s.code, i, r, s.result)
}
if len(cotp.DisallowReuse) != len(s.disallowed) {
t.Errorf("timeCode(%d) (step %d) failed: disallowReuse len mismatch: got %d expected %d", s.code, i, len(cotp.DisallowReuse), len(s.disallowed))
} else {
same := true
for j := range s.disallowed {
if s.disallowed[j] != cotp.DisallowReuse[j] {
same = false
}
}
if !same {
t.Errorf("timeCode(%d) (step %d) failed: disallowReused: got %v expected %v", s.code, i, cotp.DisallowReuse, s.disallowed)
}
}
}
}
func TestAuthenticate(t *testing.T) {
otpconf := &OTPConfig{
Secret: "2SH3V3GDW7ZNMGYE",
WindowSize: 3,
HotpCounter: 1,
ScratchCodes: []int{11112222, 22223333},
}
type attempt struct {
code string
result bool
}
var attempts = []attempt{
{"foobar", false}, // not digits
{"1fooba", false}, // not valid number
{"1111111", false}, // bad length
{ /* 1 */ "293240", true}, // hopt increments on success
{ /* 1 */ "293240", false}, // hopt failure
{"33334444", false}, // scratch
{"11112222", true},
{"11112222", false},
}
for _, a := range attempts {
r, _ := otpconf.Authenticate(a.code)
if r != a.result {
t.Errorf("bad result from code=%s: got %t expected %t\n", a.code, r, a.result)
}
}
// let's check some time-based codes
otpconf.HotpCounter = 0
// I haven't mocked the clock, so we'll just compute one
var t0 int64
if otpconf.UTC {
t0 = int64(time.Now().UTC().Unix() / 30)
} else {
t0 = int64(time.Now().Unix() / 30)
}
c := ComputeCode(otpconf.Secret, t0)
invalid := c + 1
attempts = []attempt{
{strconv.Itoa(invalid), false},
{strconv.Itoa(c), true},
}
for _, a := range attempts {
r, _ := otpconf.Authenticate(a.code)
if r != a.result {
t.Errorf("bad result from code=%s: got %t expected %t\n", a.code, r, a.result)
}
otpconf.UTC = true
r, _ = otpconf.Authenticate(a.code)
if r != a.result {
t.Errorf("bad result from code=%s: got %t expected %t\n", a.code, r, a.result)
}
otpconf.UTC = false
}
}
func TestProvisionURI(t *testing.T) {
otpconf := OTPConfig{
Secret: "x",
}
cases := []struct {
user, iss string
hotp bool
out string
}{
{"test", "", false, "otpauth://totp/test?secret=x"},
{"test", "", true, "otpauth://hotp/test?counter=1&secret=x"},
{"test", "Company", true, "otpauth://hotp/Company:test?counter=1&issuer=Company&secret=x"},
{"test", "Company", false, "otpauth://totp/Company:test?issuer=Company&secret=x"},
}
for i, c := range cases {
otpconf.HotpCounter = 0
if c.hotp {
otpconf.HotpCounter = 1
}
got := otpconf.ProvisionURIWithIssuer(c.user, c.iss)
if got != c.out {
t.Errorf("%d: want %q, got %q", i, c.out, got)
}
}
}

View file

@ -0,0 +1,8 @@
# Copyright 2010 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
include $(GOROOT)/src/Make.inc
TARG=rsc.googlecode.com/hg/gf256
GOFILES=gf256.go #rs.go
include $(GOROOT)/src/Make.pkg

View file

@ -0,0 +1,85 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains a straightforward implementation of
// Reed-Solomon encoding, along with a benchmark.
// It goes with http://research.swtch.com/field.
//
// For an optimized implementation, see gf256.go.
package gf256
import (
"bytes"
"fmt"
"testing"
)
// BlogECC writes to check the error correcting code bytes
// for data using the given Reed-Solomon parameters.
func BlogECC(rs *RSEncoder, m []byte, check []byte) {
if len(check) < rs.c {
panic("gf256: invalid check byte length")
}
if rs.c == 0 {
return
}
// The check bytes are the remainder after dividing
// data padded with c zeros by the generator polynomial.
// p = data padded with c zeros.
var p []byte
n := len(m) + rs.c
if len(rs.p) >= n {
p = rs.p
} else {
p = make([]byte, n)
}
copy(p, m)
for i := len(m); i < len(p); i++ {
p[i] = 0
}
gen := rs.gen
// Divide p by gen, leaving the remainder in p[len(data):].
// p[0] is the most significant term in p, and
// gen[0] is the most significant term in the generator.
for i := 0; i < len(m); i++ {
k := f.Mul(p[i], f.Inv(gen[0])) // k = pi / g0
// p -= k·g
for j, g := range gen {
p[i+j] = f.Add(p[i+j], f.Mul(k, g))
}
}
copy(check, p[len(m):])
rs.p = p
}
func BenchmarkBlogECC(b *testing.B) {
data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11}
check := []byte{0x29, 0x41, 0xb3, 0x93, 0x8, 0xe8, 0xa3, 0xe7, 0x63, 0x8f}
out := make([]byte, len(check))
rs := NewRSEncoder(f, len(check))
for i := 0; i < b.N; i++ {
BlogECC(rs, data, out)
}
b.SetBytes(int64(len(data)))
if !bytes.Equal(out, check) {
fmt.Printf("have %#v want %#v\n", out, check)
}
}
func TestBlogECC(t *testing.T) {
data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11}
check := []byte{0xa5, 0x24, 0xd4, 0xc1, 0xed, 0x36, 0xc7, 0x87, 0x2c, 0x55}
out := make([]byte, len(check))
rs := NewRSEncoder(f, len(check))
BlogECC(rs, data, out)
if !bytes.Equal(out, check) {
t.Errorf("have %x want %x", out, check)
}
}

View file

@ -0,0 +1,241 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package gf256 implements arithmetic over the Galois Field GF(256).
package gf256
import "strconv"
// A Field represents an instance of GF(256) defined by a specific polynomial.
type Field struct {
log [256]byte // log[0] is unused
exp [510]byte
}
// NewField returns a new field corresponding to the polynomial poly
// and generator α. The Reed-Solomon encoding in QR codes uses
// polynomial 0x11d with generator 2.
//
// The choice of generator α only affects the Exp and Log operations.
func NewField(poly, α int) *Field {
if poly < 0x100 || poly >= 0x200 || reducible(poly) {
panic("gf256: invalid polynomial: " + strconv.Itoa(poly))
}
var f Field
x := 1
for i := 0; i < 255; i++ {
if x == 1 && i != 0 {
panic("gf256: invalid generator " + strconv.Itoa(α) +
" for polynomial " + strconv.Itoa(poly))
}
f.exp[i] = byte(x)
f.exp[i+255] = byte(x)
f.log[x] = byte(i)
x = mul(x, α, poly)
}
f.log[0] = 255
for i := 0; i < 255; i++ {
if f.log[f.exp[i]] != byte(i) {
panic("bad log")
}
if f.log[f.exp[i+255]] != byte(i) {
panic("bad log")
}
}
for i := 1; i < 256; i++ {
if f.exp[f.log[i]] != byte(i) {
panic("bad log")
}
}
return &f
}
// nbit returns the number of significant in p.
func nbit(p int) uint {
n := uint(0)
for ; p > 0; p >>= 1 {
n++
}
return n
}
// polyDiv divides the polynomial p by q and returns the remainder.
func polyDiv(p, q int) int {
np := nbit(p)
nq := nbit(q)
for ; np >= nq; np-- {
if p&(1<<(np-1)) != 0 {
p ^= q << (np - nq)
}
}
return p
}
// mul returns the product x*y mod poly, a GF(256) multiplication.
func mul(x, y, poly int) int {
z := 0
for x > 0 {
if x&1 != 0 {
z ^= y
}
x >>= 1
y <<= 1
if y&0x100 != 0 {
y ^= poly
}
}
return z
}
// reducible reports whether p is reducible.
func reducible(p int) bool {
// Multiplying n-bit * n-bit produces (2n-1)-bit,
// so if p is reducible, one of its factors must be
// of np/2+1 bits or fewer.
np := nbit(p)
for q := 2; q < 1<<(np/2+1); q++ {
if polyDiv(p, q) == 0 {
return true
}
}
return false
}
// Add returns the sum of x and y in the field.
func (f *Field) Add(x, y byte) byte {
return x ^ y
}
// Exp returns the the base-α exponential of e in the field.
// If e < 0, Exp returns 0.
func (f *Field) Exp(e int) byte {
if e < 0 {
return 0
}
return f.exp[e%255]
}
// Log returns the base-α logarithm of x in the field.
// If x == 0, Log returns -1.
func (f *Field) Log(x byte) int {
if x == 0 {
return -1
}
return int(f.log[x])
}
// Inv returns the multiplicative inverse of x in the field.
// If x == 0, Inv returns 0.
func (f *Field) Inv(x byte) byte {
if x == 0 {
return 0
}
return f.exp[255-f.log[x]]
}
// Mul returns the product of x and y in the field.
func (f *Field) Mul(x, y byte) byte {
if x == 0 || y == 0 {
return 0
}
return f.exp[int(f.log[x])+int(f.log[y])]
}
// An RSEncoder implements Reed-Solomon encoding
// over a given field using a given number of error correction bytes.
type RSEncoder struct {
f *Field
c int
gen []byte
lgen []byte
p []byte
}
func (f *Field) gen(e int) (gen, lgen []byte) {
// p = 1
p := make([]byte, e+1)
p[e] = 1
for i := 0; i < e; i++ {
// p *= (x + Exp(i))
// p[j] = p[j]*Exp(i) + p[j+1].
c := f.Exp(i)
for j := 0; j < e; j++ {
p[j] = f.Mul(p[j], c) ^ p[j+1]
}
p[e] = f.Mul(p[e], c)
}
// lp = log p.
lp := make([]byte, e+1)
for i, c := range p {
if c == 0 {
lp[i] = 255
} else {
lp[i] = byte(f.Log(c))
}
}
return p, lp
}
// NewRSEncoder returns a new Reed-Solomon encoder
// over the given field and number of error correction bytes.
func NewRSEncoder(f *Field, c int) *RSEncoder {
gen, lgen := f.gen(c)
return &RSEncoder{f: f, c: c, gen: gen, lgen: lgen}
}
// ECC writes to check the error correcting code bytes
// for data using the given Reed-Solomon parameters.
func (rs *RSEncoder) ECC(data []byte, check []byte) {
if len(check) < rs.c {
panic("gf256: invalid check byte length")
}
if rs.c == 0 {
return
}
// The check bytes are the remainder after dividing
// data padded with c zeros by the generator polynomial.
// p = data padded with c zeros.
var p []byte
n := len(data) + rs.c
if len(rs.p) >= n {
p = rs.p
} else {
p = make([]byte, n)
}
copy(p, data)
for i := len(data); i < len(p); i++ {
p[i] = 0
}
// Divide p by gen, leaving the remainder in p[len(data):].
// p[0] is the most significant term in p, and
// gen[0] is the most significant term in the generator,
// which is always 1.
// To avoid repeated work, we store various values as
// lv, not v, where lv = log[v].
f := rs.f
lgen := rs.lgen[1:]
for i := 0; i < len(data); i++ {
c := p[i]
if c == 0 {
continue
}
q := p[i+1:]
exp := f.exp[f.log[c]:]
for j, lg := range lgen {
if lg != 255 { // lgen uses 255 for log 0
q[j] ^= exp[lg]
}
}
}
copy(check, p[len(data):])
rs.p = p
}

View file

@ -0,0 +1,194 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gf256
import (
"bytes"
"fmt"
"testing"
)
var f = NewField(0x11d, 2) // x^8 + x^4 + x^3 + x^2 + 1
func TestBasic(t *testing.T) {
if f.Exp(0) != 1 || f.Exp(1) != 2 || f.Exp(255) != 1 {
panic("bad Exp")
}
}
func TestECC(t *testing.T) {
data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11}
check := []byte{0xa5, 0x24, 0xd4, 0xc1, 0xed, 0x36, 0xc7, 0x87, 0x2c, 0x55}
out := make([]byte, len(check))
rs := NewRSEncoder(f, len(check))
rs.ECC(data, out)
if !bytes.Equal(out, check) {
t.Errorf("have %x want %x", out, check)
}
}
func TestLinear(t *testing.T) {
d1 := []byte{0x00, 0x00}
c1 := []byte{0x00, 0x00}
out := make([]byte, len(c1))
rs := NewRSEncoder(f, len(c1))
if rs.ECC(d1, out); !bytes.Equal(out, c1) {
t.Errorf("ECBytes(%x, %d) = %x, want 0", d1, len(c1), out)
}
d2 := []byte{0x00, 0x01}
c2 := make([]byte, 2)
rs.ECC(d2, c2)
d3 := []byte{0x00, 0x02}
c3 := make([]byte, 2)
rs.ECC(d3, c3)
cx := make([]byte, 2)
for i := range cx {
cx[i] = c2[i] ^ c3[i]
}
d4 := []byte{0x00, 0x03}
c4 := make([]byte, 2)
rs.ECC(d4, c4)
if !bytes.Equal(cx, c4) {
t.Errorf("ECBytes(%x, 2) = %x\nECBytes(%x, 2) = %x\nxor = %x\nECBytes(%x, 2) = %x",
d2, c2, d3, c3, cx, d4, c4)
}
}
func TestGaussJordan(t *testing.T) {
rs := NewRSEncoder(f, 2)
m := make([][]byte, 16)
for i := range m {
m[i] = make([]byte, 4)
m[i][i/8] = 1 << uint(i%8)
rs.ECC(m[i][:2], m[i][2:])
}
if false {
fmt.Printf("---\n")
for _, row := range m {
fmt.Printf("%x\n", row)
}
}
b := []uint{0, 1, 2, 3, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25, 26, 27}
for i := 0; i < 16; i++ {
bi := b[i]
if m[i][bi/8]&(1<<(7-bi%8)) == 0 {
for j := i + 1; ; j++ {
if j >= len(m) {
t.Errorf("lost track for %d", bi)
break
}
if m[j][bi/8]&(1<<(7-bi%8)) != 0 {
m[i], m[j] = m[j], m[i]
break
}
}
}
for j := i + 1; j < len(m); j++ {
if m[j][bi/8]&(1<<(7-bi%8)) != 0 {
for k := range m[j] {
m[j][k] ^= m[i][k]
}
}
}
}
if false {
fmt.Printf("---\n")
for _, row := range m {
fmt.Printf("%x\n", row)
}
}
for i := 15; i >= 0; i-- {
bi := b[i]
for j := i - 1; j >= 0; j-- {
if m[j][bi/8]&(1<<(7-bi%8)) != 0 {
for k := range m[j] {
m[j][k] ^= m[i][k]
}
}
}
}
if false {
fmt.Printf("---\n")
for _, row := range m {
fmt.Printf("%x", row)
out := make([]byte, 2)
if rs.ECC(row[:2], out); !bytes.Equal(out, row[2:]) {
fmt.Printf(" - want %x", out)
}
fmt.Printf("\n")
}
}
}
func BenchmarkECC(b *testing.B) {
data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11}
check := []byte{0x29, 0x41, 0xb3, 0x93, 0x8, 0xe8, 0xa3, 0xe7, 0x63, 0x8f}
out := make([]byte, len(check))
rs := NewRSEncoder(f, len(check))
for i := 0; i < b.N; i++ {
rs.ECC(data, out)
}
b.SetBytes(int64(len(data)))
if !bytes.Equal(out, check) {
fmt.Printf("have %#v want %#v\n", out, check)
}
}
func TestGen(t *testing.T) {
for i := 0; i < 256; i++ {
_, lg := f.gen(i)
if lg[0] != 0 {
t.Errorf("#%d: %x", i, lg)
}
}
}
func TestReducible(t *testing.T) {
var count = []int{1, 2, 3, 6, 9, 18, 30, 56, 99, 186} // oeis.org/A1037
for i, want := range count {
n := 0
for p := 1 << uint(i+2); p < 1<<uint(i+3); p++ {
if !reducible(p) {
n++
}
}
if n != want {
t.Errorf("#reducible(%d-bit) = %d, want %d", i+2, n, want)
}
}
}
func TestExhaustive(t *testing.T) {
for poly := 0x100; poly < 0x200; poly++ {
if reducible(poly) {
continue
}
α := 2
for !generates(α, poly) {
α++
}
f := NewField(poly, α)
for p := 0; p < 256; p++ {
for q := 0; q < 256; q++ {
fm := int(f.Mul(byte(p), byte(q)))
pm := mul(p, q, poly)
if fm != pm {
t.Errorf("NewField(%#x).Mul(%#x, %#x) = %#x, want %#x", poly, p, q, fm, pm)
}
}
}
}
}
func generates(α, poly int) bool {
x := α
for i := 0; i < 254; i++ {
if x == 1 {
return false
}
x = mul(x, α, poly)
}
return true
}

View file

@ -0,0 +1,4 @@
include $(GOROOT)/src/Make.inc
TARG=rsc.googlecode.com/hg/qr
GOFILES=qr.go png.go
include $(GOROOT)/src/Make.pkg

View file

@ -0,0 +1,7 @@
include $(GOROOT)/src/Make.inc
TARG=rsc.googlecode.com/hg/qr/coding
GOFILES=\
qr.go\
include $(GOROOT)/src/Make.pkg

View file

@ -0,0 +1,149 @@
// +build ignore
package main
import "fmt"
// tables from qrencode-3.1.1/qrspec.c
var capacity = [41]struct {
width int
words int
remainder int
ec [4]int
}{
{0, 0, 0, [4]int{0, 0, 0, 0}},
{21, 26, 0, [4]int{7, 10, 13, 17}}, // 1
{25, 44, 7, [4]int{10, 16, 22, 28}},
{29, 70, 7, [4]int{15, 26, 36, 44}},
{33, 100, 7, [4]int{20, 36, 52, 64}},
{37, 134, 7, [4]int{26, 48, 72, 88}}, // 5
{41, 172, 7, [4]int{36, 64, 96, 112}},
{45, 196, 0, [4]int{40, 72, 108, 130}},
{49, 242, 0, [4]int{48, 88, 132, 156}},
{53, 292, 0, [4]int{60, 110, 160, 192}},
{57, 346, 0, [4]int{72, 130, 192, 224}}, //10
{61, 404, 0, [4]int{80, 150, 224, 264}},
{65, 466, 0, [4]int{96, 176, 260, 308}},
{69, 532, 0, [4]int{104, 198, 288, 352}},
{73, 581, 3, [4]int{120, 216, 320, 384}},
{77, 655, 3, [4]int{132, 240, 360, 432}}, //15
{81, 733, 3, [4]int{144, 280, 408, 480}},
{85, 815, 3, [4]int{168, 308, 448, 532}},
{89, 901, 3, [4]int{180, 338, 504, 588}},
{93, 991, 3, [4]int{196, 364, 546, 650}},
{97, 1085, 3, [4]int{224, 416, 600, 700}}, //20
{101, 1156, 4, [4]int{224, 442, 644, 750}},
{105, 1258, 4, [4]int{252, 476, 690, 816}},
{109, 1364, 4, [4]int{270, 504, 750, 900}},
{113, 1474, 4, [4]int{300, 560, 810, 960}},
{117, 1588, 4, [4]int{312, 588, 870, 1050}}, //25
{121, 1706, 4, [4]int{336, 644, 952, 1110}},
{125, 1828, 4, [4]int{360, 700, 1020, 1200}},
{129, 1921, 3, [4]int{390, 728, 1050, 1260}},
{133, 2051, 3, [4]int{420, 784, 1140, 1350}},
{137, 2185, 3, [4]int{450, 812, 1200, 1440}}, //30
{141, 2323, 3, [4]int{480, 868, 1290, 1530}},
{145, 2465, 3, [4]int{510, 924, 1350, 1620}},
{149, 2611, 3, [4]int{540, 980, 1440, 1710}},
{153, 2761, 3, [4]int{570, 1036, 1530, 1800}},
{157, 2876, 0, [4]int{570, 1064, 1590, 1890}}, //35
{161, 3034, 0, [4]int{600, 1120, 1680, 1980}},
{165, 3196, 0, [4]int{630, 1204, 1770, 2100}},
{169, 3362, 0, [4]int{660, 1260, 1860, 2220}},
{173, 3532, 0, [4]int{720, 1316, 1950, 2310}},
{177, 3706, 0, [4]int{750, 1372, 2040, 2430}}, //40
}
var eccTable = [41][4][2]int{
{{0, 0}, {0, 0}, {0, 0}, {0, 0}},
{{1, 0}, {1, 0}, {1, 0}, {1, 0}}, // 1
{{1, 0}, {1, 0}, {1, 0}, {1, 0}},
{{1, 0}, {1, 0}, {2, 0}, {2, 0}},
{{1, 0}, {2, 0}, {2, 0}, {4, 0}},
{{1, 0}, {2, 0}, {2, 2}, {2, 2}}, // 5
{{2, 0}, {4, 0}, {4, 0}, {4, 0}},
{{2, 0}, {4, 0}, {2, 4}, {4, 1}},
{{2, 0}, {2, 2}, {4, 2}, {4, 2}},
{{2, 0}, {3, 2}, {4, 4}, {4, 4}},
{{2, 2}, {4, 1}, {6, 2}, {6, 2}}, //10
{{4, 0}, {1, 4}, {4, 4}, {3, 8}},
{{2, 2}, {6, 2}, {4, 6}, {7, 4}},
{{4, 0}, {8, 1}, {8, 4}, {12, 4}},
{{3, 1}, {4, 5}, {11, 5}, {11, 5}},
{{5, 1}, {5, 5}, {5, 7}, {11, 7}}, //15
{{5, 1}, {7, 3}, {15, 2}, {3, 13}},
{{1, 5}, {10, 1}, {1, 15}, {2, 17}},
{{5, 1}, {9, 4}, {17, 1}, {2, 19}},
{{3, 4}, {3, 11}, {17, 4}, {9, 16}},
{{3, 5}, {3, 13}, {15, 5}, {15, 10}}, //20
{{4, 4}, {17, 0}, {17, 6}, {19, 6}},
{{2, 7}, {17, 0}, {7, 16}, {34, 0}},
{{4, 5}, {4, 14}, {11, 14}, {16, 14}},
{{6, 4}, {6, 14}, {11, 16}, {30, 2}},
{{8, 4}, {8, 13}, {7, 22}, {22, 13}}, //25
{{10, 2}, {19, 4}, {28, 6}, {33, 4}},
{{8, 4}, {22, 3}, {8, 26}, {12, 28}},
{{3, 10}, {3, 23}, {4, 31}, {11, 31}},
{{7, 7}, {21, 7}, {1, 37}, {19, 26}},
{{5, 10}, {19, 10}, {15, 25}, {23, 25}}, //30
{{13, 3}, {2, 29}, {42, 1}, {23, 28}},
{{17, 0}, {10, 23}, {10, 35}, {19, 35}},
{{17, 1}, {14, 21}, {29, 19}, {11, 46}},
{{13, 6}, {14, 23}, {44, 7}, {59, 1}},
{{12, 7}, {12, 26}, {39, 14}, {22, 41}}, //35
{{6, 14}, {6, 34}, {46, 10}, {2, 64}},
{{17, 4}, {29, 14}, {49, 10}, {24, 46}},
{{4, 18}, {13, 32}, {48, 14}, {42, 32}},
{{20, 4}, {40, 7}, {43, 22}, {10, 67}},
{{19, 6}, {18, 31}, {34, 34}, {20, 61}}, //40
}
var align = [41][2]int{
{0, 0},
{0, 0}, {18, 0}, {22, 0}, {26, 0}, {30, 0}, // 1- 5
{34, 0}, {22, 38}, {24, 42}, {26, 46}, {28, 50}, // 6-10
{30, 54}, {32, 58}, {34, 62}, {26, 46}, {26, 48}, //11-15
{26, 50}, {30, 54}, {30, 56}, {30, 58}, {34, 62}, //16-20
{28, 50}, {26, 50}, {30, 54}, {28, 54}, {32, 58}, //21-25
{30, 58}, {34, 62}, {26, 50}, {30, 54}, {26, 52}, //26-30
{30, 56}, {34, 60}, {30, 58}, {34, 62}, {30, 54}, //31-35
{24, 50}, {28, 54}, {32, 58}, {26, 54}, {30, 58}, //35-40
}
var versionPattern = [41]int{
0,
0, 0, 0, 0, 0, 0,
0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d,
0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9,
0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75,
0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64,
0x27541, 0x28c69,
}
func main() {
fmt.Printf("\t{},\n")
for i := 1; i <= 40; i++ {
apos := align[i][0] - 2
if apos < 0 {
apos = 100
}
astride := align[i][1] - align[i][0]
if astride < 1 {
astride = 100
}
fmt.Printf("\t{%v, %v, %v, %#x, [4]level{{%v, %v}, {%v, %v}, {%v, %v}, {%v, %v}}}, // %v\n",
apos, astride, capacity[i].words,
versionPattern[i],
eccTable[i][0][0]+eccTable[i][0][1],
float64(capacity[i].ec[0])/float64(eccTable[i][0][0]+eccTable[i][0][1]),
eccTable[i][1][0]+eccTable[i][1][1],
float64(capacity[i].ec[1])/float64(eccTable[i][1][0]+eccTable[i][1][1]),
eccTable[i][2][0]+eccTable[i][2][1],
float64(capacity[i].ec[2])/float64(eccTable[i][2][0]+eccTable[i][2][1]),
eccTable[i][3][0]+eccTable[i][3][1],
float64(capacity[i].ec[3])/float64(eccTable[i][3][0]+eccTable[i][3][1]),
i,
)
}
}

View file

@ -0,0 +1,815 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package coding implements low-level QR coding details.
package coding
import (
"fmt"
"strconv"
"strings"
"github.com/mattermost/rsc/gf256"
)
// Field is the field for QR error correction.
var Field = gf256.NewField(0x11d, 2)
// A Version represents a QR version.
// The version specifies the size of the QR code:
// a QR code with version v has 4v+17 pixels on a side.
// Versions number from 1 to 40: the larger the version,
// the more information the code can store.
type Version int
const MinVersion = 1
const MaxVersion = 40
func (v Version) String() string {
return strconv.Itoa(int(v))
}
func (v Version) sizeClass() int {
if v <= 9 {
return 0
}
if v <= 26 {
return 1
}
return 2
}
// DataBytes returns the number of data bytes that can be
// stored in a QR code with the given version and level.
func (v Version) DataBytes(l Level) int {
vt := &vtab[v]
lev := &vt.level[l]
return vt.bytes - lev.nblock*lev.check
}
// Encoding implements a QR data encoding scheme.
// The implementations--Numeric, Alphanumeric, and String--specify
// the character set and the mapping from UTF-8 to code bits.
// The more restrictive the mode, the fewer code bits are needed.
type Encoding interface {
Check() error
Bits(v Version) int
Encode(b *Bits, v Version)
}
type Bits struct {
b []byte
nbit int
}
func (b *Bits) Reset() {
b.b = b.b[:0]
b.nbit = 0
}
func (b *Bits) Bits() int {
return b.nbit
}
func (b *Bits) Bytes() []byte {
if b.nbit%8 != 0 {
panic("fractional byte")
}
return b.b
}
func (b *Bits) Append(p []byte) {
if b.nbit%8 != 0 {
panic("fractional byte")
}
b.b = append(b.b, p...)
b.nbit += 8 * len(p)
}
func (b *Bits) Write(v uint, nbit int) {
for nbit > 0 {
n := nbit
if n > 8 {
n = 8
}
if b.nbit%8 == 0 {
b.b = append(b.b, 0)
} else {
m := -b.nbit & 7
if n > m {
n = m
}
}
b.nbit += n
sh := uint(nbit - n)
b.b[len(b.b)-1] |= uint8(v >> sh << uint(-b.nbit&7))
v -= v >> sh << sh
nbit -= n
}
}
// Num is the encoding for numeric data.
// The only valid characters are the decimal digits 0 through 9.
type Num string
func (s Num) String() string {
return fmt.Sprintf("Num(%#q)", string(s))
}
func (s Num) Check() error {
for _, c := range s {
if c < '0' || '9' < c {
return fmt.Errorf("non-numeric string %#q", string(s))
}
}
return nil
}
var numLen = [3]int{10, 12, 14}
func (s Num) Bits(v Version) int {
return 4 + numLen[v.sizeClass()] + (10*len(s)+2)/3
}
func (s Num) Encode(b *Bits, v Version) {
b.Write(1, 4)
b.Write(uint(len(s)), numLen[v.sizeClass()])
var i int
for i = 0; i+3 <= len(s); i += 3 {
w := uint(s[i]-'0')*100 + uint(s[i+1]-'0')*10 + uint(s[i+2]-'0')
b.Write(w, 10)
}
switch len(s) - i {
case 1:
w := uint(s[i] - '0')
b.Write(w, 4)
case 2:
w := uint(s[i]-'0')*10 + uint(s[i+1]-'0')
b.Write(w, 7)
}
}
// Alpha is the encoding for alphanumeric data.
// The valid characters are 0-9A-Z$%*+-./: and space.
type Alpha string
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
func (s Alpha) String() string {
return fmt.Sprintf("Alpha(%#q)", string(s))
}
func (s Alpha) Check() error {
for _, c := range s {
if strings.IndexRune(alphabet, c) < 0 {
return fmt.Errorf("non-alphanumeric string %#q", string(s))
}
}
return nil
}
var alphaLen = [3]int{9, 11, 13}
func (s Alpha) Bits(v Version) int {
return 4 + alphaLen[v.sizeClass()] + (11*len(s)+1)/2
}
func (s Alpha) Encode(b *Bits, v Version) {
b.Write(2, 4)
b.Write(uint(len(s)), alphaLen[v.sizeClass()])
var i int
for i = 0; i+2 <= len(s); i += 2 {
w := uint(strings.IndexRune(alphabet, rune(s[i])))*45 +
uint(strings.IndexRune(alphabet, rune(s[i+1])))
b.Write(w, 11)
}
if i < len(s) {
w := uint(strings.IndexRune(alphabet, rune(s[i])))
b.Write(w, 6)
}
}
// String is the encoding for 8-bit data. All bytes are valid.
type String string
func (s String) String() string {
return fmt.Sprintf("String(%#q)", string(s))
}
func (s String) Check() error {
return nil
}
var stringLen = [3]int{8, 16, 16}
func (s String) Bits(v Version) int {
return 4 + stringLen[v.sizeClass()] + 8*len(s)
}
func (s String) Encode(b *Bits, v Version) {
b.Write(4, 4)
b.Write(uint(len(s)), stringLen[v.sizeClass()])
for i := 0; i < len(s); i++ {
b.Write(uint(s[i]), 8)
}
}
// A Pixel describes a single pixel in a QR code.
type Pixel uint32
const (
Black Pixel = 1 << iota
Invert
)
func (p Pixel) Offset() uint {
return uint(p >> 6)
}
func OffsetPixel(o uint) Pixel {
return Pixel(o << 6)
}
func (r PixelRole) Pixel() Pixel {
return Pixel(r << 2)
}
func (p Pixel) Role() PixelRole {
return PixelRole(p>>2) & 15
}
func (p Pixel) String() string {
s := p.Role().String()
if p&Black != 0 {
s += "+black"
}
if p&Invert != 0 {
s += "+invert"
}
s += "+" + strconv.FormatUint(uint64(p.Offset()), 10)
return s
}
// A PixelRole describes the role of a QR pixel.
type PixelRole uint32
const (
_ PixelRole = iota
Position // position squares (large)
Alignment // alignment squares (small)
Timing // timing strip between position squares
Format // format metadata
PVersion // version pattern
Unused // unused pixel
Data // data bit
Check // error correction check bit
Extra
)
var roles = []string{
"",
"position",
"alignment",
"timing",
"format",
"pversion",
"unused",
"data",
"check",
"extra",
}
func (r PixelRole) String() string {
if Position <= r && r <= Check {
return roles[r]
}
return strconv.Itoa(int(r))
}
// A Level represents a QR error correction level.
// From least to most tolerant of errors, they are L, M, Q, H.
type Level int
const (
L Level = iota
M
Q
H
)
func (l Level) String() string {
if L <= l && l <= H {
return "LMQH"[l : l+1]
}
return strconv.Itoa(int(l))
}
// A Code is a square pixel grid.
type Code struct {
Bitmap []byte // 1 is black, 0 is white
Size int // number of pixels on a side
Stride int // number of bytes per row
}
func (c *Code) Black(x, y int) bool {
return 0 <= x && x < c.Size && 0 <= y && y < c.Size &&
c.Bitmap[y*c.Stride+x/8]&(1<<uint(7-x&7)) != 0
}
// A Mask describes a mask that is applied to the QR
// code to avoid QR artifacts being interpreted as
// alignment and timing patterns (such as the squares
// in the corners). Valid masks are integers from 0 to 7.
type Mask int
// http://www.swetake.com/qr/qr5_en.html
var mfunc = []func(int, int) bool{
func(i, j int) bool { return (i+j)%2 == 0 },
func(i, j int) bool { return i%2 == 0 },
func(i, j int) bool { return j%3 == 0 },
func(i, j int) bool { return (i+j)%3 == 0 },
func(i, j int) bool { return (i/2+j/3)%2 == 0 },
func(i, j int) bool { return i*j%2+i*j%3 == 0 },
func(i, j int) bool { return (i*j%2+i*j%3)%2 == 0 },
func(i, j int) bool { return (i*j%3+(i+j)%2)%2 == 0 },
}
func (m Mask) Invert(y, x int) bool {
if m < 0 {
return false
}
return mfunc[m](y, x)
}
// A Plan describes how to construct a QR code
// with a specific version, level, and mask.
type Plan struct {
Version Version
Level Level
Mask Mask
DataBytes int // number of data bytes
CheckBytes int // number of error correcting (checksum) bytes
Blocks int // number of data blocks
Pixel [][]Pixel // pixel map
}
// NewPlan returns a Plan for a QR code with the given
// version, level, and mask.
func NewPlan(version Version, level Level, mask Mask) (*Plan, error) {
p, err := vplan(version)
if err != nil {
return nil, err
}
if err := fplan(level, mask, p); err != nil {
return nil, err
}
if err := lplan(version, level, p); err != nil {
return nil, err
}
if err := mplan(mask, p); err != nil {
return nil, err
}
return p, nil
}
func (b *Bits) Pad(n int) {
if n < 0 {
panic("qr: invalid pad size")
}
if n <= 4 {
b.Write(0, n)
} else {
b.Write(0, 4)
n -= 4
n -= -b.Bits() & 7
b.Write(0, -b.Bits()&7)
pad := n / 8
for i := 0; i < pad; i += 2 {
b.Write(0xec, 8)
if i+1 >= pad {
break
}
b.Write(0x11, 8)
}
}
}
func (b *Bits) AddCheckBytes(v Version, l Level) {
nd := v.DataBytes(l)
if b.nbit < nd*8 {
b.Pad(nd*8 - b.nbit)
}
if b.nbit != nd*8 {
panic("qr: too much data")
}
dat := b.Bytes()
vt := &vtab[v]
lev := &vt.level[l]
db := nd / lev.nblock
extra := nd % lev.nblock
chk := make([]byte, lev.check)
rs := gf256.NewRSEncoder(Field, lev.check)
for i := 0; i < lev.nblock; i++ {
if i == lev.nblock-extra {
db++
}
rs.ECC(dat[:db], chk)
b.Append(chk)
dat = dat[db:]
}
if len(b.Bytes()) != vt.bytes {
panic("qr: internal error")
}
}
func (p *Plan) Encode(text ...Encoding) (*Code, error) {
var b Bits
for _, t := range text {
if err := t.Check(); err != nil {
return nil, err
}
t.Encode(&b, p.Version)
}
if b.Bits() > p.DataBytes*8 {
return nil, fmt.Errorf("cannot encode %d bits into %d-bit code", b.Bits(), p.DataBytes*8)
}
b.AddCheckBytes(p.Version, p.Level)
bytes := b.Bytes()
// Now we have the checksum bytes and the data bytes.
// Construct the actual code.
c := &Code{Size: len(p.Pixel), Stride: (len(p.Pixel) + 7) &^ 7}
c.Bitmap = make([]byte, c.Stride*c.Size)
crow := c.Bitmap
for _, row := range p.Pixel {
for x, pix := range row {
switch pix.Role() {
case Data, Check:
o := pix.Offset()
if bytes[o/8]&(1<<uint(7-o&7)) != 0 {
pix ^= Black
}
}
if pix&Black != 0 {
crow[x/8] |= 1 << uint(7-x&7)
}
}
crow = crow[c.Stride:]
}
return c, nil
}
// A version describes metadata associated with a version.
type version struct {
apos int
astride int
bytes int
pattern int
level [4]level
}
type level struct {
nblock int
check int
}
var vtab = []version{
{},
{100, 100, 26, 0x0, [4]level{{1, 7}, {1, 10}, {1, 13}, {1, 17}}}, // 1
{16, 100, 44, 0x0, [4]level{{1, 10}, {1, 16}, {1, 22}, {1, 28}}}, // 2
{20, 100, 70, 0x0, [4]level{{1, 15}, {1, 26}, {2, 18}, {2, 22}}}, // 3
{24, 100, 100, 0x0, [4]level{{1, 20}, {2, 18}, {2, 26}, {4, 16}}}, // 4
{28, 100, 134, 0x0, [4]level{{1, 26}, {2, 24}, {4, 18}, {4, 22}}}, // 5
{32, 100, 172, 0x0, [4]level{{2, 18}, {4, 16}, {4, 24}, {4, 28}}}, // 6
{20, 16, 196, 0x7c94, [4]level{{2, 20}, {4, 18}, {6, 18}, {5, 26}}}, // 7
{22, 18, 242, 0x85bc, [4]level{{2, 24}, {4, 22}, {6, 22}, {6, 26}}}, // 8
{24, 20, 292, 0x9a99, [4]level{{2, 30}, {5, 22}, {8, 20}, {8, 24}}}, // 9
{26, 22, 346, 0xa4d3, [4]level{{4, 18}, {5, 26}, {8, 24}, {8, 28}}}, // 10
{28, 24, 404, 0xbbf6, [4]level{{4, 20}, {5, 30}, {8, 28}, {11, 24}}}, // 11
{30, 26, 466, 0xc762, [4]level{{4, 24}, {8, 22}, {10, 26}, {11, 28}}}, // 12
{32, 28, 532, 0xd847, [4]level{{4, 26}, {9, 22}, {12, 24}, {16, 22}}}, // 13
{24, 20, 581, 0xe60d, [4]level{{4, 30}, {9, 24}, {16, 20}, {16, 24}}}, // 14
{24, 22, 655, 0xf928, [4]level{{6, 22}, {10, 24}, {12, 30}, {18, 24}}}, // 15
{24, 24, 733, 0x10b78, [4]level{{6, 24}, {10, 28}, {17, 24}, {16, 30}}}, // 16
{28, 24, 815, 0x1145d, [4]level{{6, 28}, {11, 28}, {16, 28}, {19, 28}}}, // 17
{28, 26, 901, 0x12a17, [4]level{{6, 30}, {13, 26}, {18, 28}, {21, 28}}}, // 18
{28, 28, 991, 0x13532, [4]level{{7, 28}, {14, 26}, {21, 26}, {25, 26}}}, // 19
{32, 28, 1085, 0x149a6, [4]level{{8, 28}, {16, 26}, {20, 30}, {25, 28}}}, // 20
{26, 22, 1156, 0x15683, [4]level{{8, 28}, {17, 26}, {23, 28}, {25, 30}}}, // 21
{24, 24, 1258, 0x168c9, [4]level{{9, 28}, {17, 28}, {23, 30}, {34, 24}}}, // 22
{28, 24, 1364, 0x177ec, [4]level{{9, 30}, {18, 28}, {25, 30}, {30, 30}}}, // 23
{26, 26, 1474, 0x18ec4, [4]level{{10, 30}, {20, 28}, {27, 30}, {32, 30}}}, // 24
{30, 26, 1588, 0x191e1, [4]level{{12, 26}, {21, 28}, {29, 30}, {35, 30}}}, // 25
{28, 28, 1706, 0x1afab, [4]level{{12, 28}, {23, 28}, {34, 28}, {37, 30}}}, // 26
{32, 28, 1828, 0x1b08e, [4]level{{12, 30}, {25, 28}, {34, 30}, {40, 30}}}, // 27
{24, 24, 1921, 0x1cc1a, [4]level{{13, 30}, {26, 28}, {35, 30}, {42, 30}}}, // 28
{28, 24, 2051, 0x1d33f, [4]level{{14, 30}, {28, 28}, {38, 30}, {45, 30}}}, // 29
{24, 26, 2185, 0x1ed75, [4]level{{15, 30}, {29, 28}, {40, 30}, {48, 30}}}, // 30
{28, 26, 2323, 0x1f250, [4]level{{16, 30}, {31, 28}, {43, 30}, {51, 30}}}, // 31
{32, 26, 2465, 0x209d5, [4]level{{17, 30}, {33, 28}, {45, 30}, {54, 30}}}, // 32
{28, 28, 2611, 0x216f0, [4]level{{18, 30}, {35, 28}, {48, 30}, {57, 30}}}, // 33
{32, 28, 2761, 0x228ba, [4]level{{19, 30}, {37, 28}, {51, 30}, {60, 30}}}, // 34
{28, 24, 2876, 0x2379f, [4]level{{19, 30}, {38, 28}, {53, 30}, {63, 30}}}, // 35
{22, 26, 3034, 0x24b0b, [4]level{{20, 30}, {40, 28}, {56, 30}, {66, 30}}}, // 36
{26, 26, 3196, 0x2542e, [4]level{{21, 30}, {43, 28}, {59, 30}, {70, 30}}}, // 37
{30, 26, 3362, 0x26a64, [4]level{{22, 30}, {45, 28}, {62, 30}, {74, 30}}}, // 38
{24, 28, 3532, 0x27541, [4]level{{24, 30}, {47, 28}, {65, 30}, {77, 30}}}, // 39
{28, 28, 3706, 0x28c69, [4]level{{25, 30}, {49, 28}, {68, 30}, {81, 30}}}, // 40
}
func grid(siz int) [][]Pixel {
m := make([][]Pixel, siz)
pix := make([]Pixel, siz*siz)
for i := range m {
m[i], pix = pix[:siz], pix[siz:]
}
return m
}
// vplan creates a Plan for the given version.
func vplan(v Version) (*Plan, error) {
p := &Plan{Version: v}
if v < 1 || v > 40 {
return nil, fmt.Errorf("invalid QR version %d", int(v))
}
siz := 17 + int(v)*4
m := grid(siz)
p.Pixel = m
// Timing markers (overwritten by boxes).
const ti = 6 // timing is in row/column 6 (counting from 0)
for i := range m {
p := Timing.Pixel()
if i&1 == 0 {
p |= Black
}
m[i][ti] = p
m[ti][i] = p
}
// Position boxes.
posBox(m, 0, 0)
posBox(m, siz-7, 0)
posBox(m, 0, siz-7)
// Alignment boxes.
info := &vtab[v]
for x := 4; x+5 < siz; {
for y := 4; y+5 < siz; {
// don't overwrite timing markers
if (x < 7 && y < 7) || (x < 7 && y+5 >= siz-7) || (x+5 >= siz-7 && y < 7) {
} else {
alignBox(m, x, y)
}
if y == 4 {
y = info.apos
} else {
y += info.astride
}
}
if x == 4 {
x = info.apos
} else {
x += info.astride
}
}
// Version pattern.
pat := vtab[v].pattern
if pat != 0 {
v := pat
for x := 0; x < 6; x++ {
for y := 0; y < 3; y++ {
p := PVersion.Pixel()
if v&1 != 0 {
p |= Black
}
m[siz-11+y][x] = p
m[x][siz-11+y] = p
v >>= 1
}
}
}
// One lonely black pixel
m[siz-8][8] = Unused.Pixel() | Black
return p, nil
}
// fplan adds the format pixels
func fplan(l Level, m Mask, p *Plan) error {
// Format pixels.
fb := uint32(l^1) << 13 // level: L=01, M=00, Q=11, H=10
fb |= uint32(m) << 10 // mask
const formatPoly = 0x537
rem := fb
for i := 14; i >= 10; i-- {
if rem&(1<<uint(i)) != 0 {
rem ^= formatPoly << uint(i-10)
}
}
fb |= rem
invert := uint32(0x5412)
siz := len(p.Pixel)
for i := uint(0); i < 15; i++ {
pix := Format.Pixel() + OffsetPixel(i)
if (fb>>i)&1 == 1 {
pix |= Black
}
if (invert>>i)&1 == 1 {
pix ^= Invert | Black
}
// top left
switch {
case i < 6:
p.Pixel[i][8] = pix
case i < 8:
p.Pixel[i+1][8] = pix
case i < 9:
p.Pixel[8][7] = pix
default:
p.Pixel[8][14-i] = pix
}
// bottom right
switch {
case i < 8:
p.Pixel[8][siz-1-int(i)] = pix
default:
p.Pixel[siz-1-int(14-i)][8] = pix
}
}
return nil
}
// lplan edits a version-only Plan to add information
// about the error correction levels.
func lplan(v Version, l Level, p *Plan) error {
p.Level = l
nblock := vtab[v].level[l].nblock
ne := vtab[v].level[l].check
nde := (vtab[v].bytes - ne*nblock) / nblock
extra := (vtab[v].bytes - ne*nblock) % nblock
dataBits := (nde*nblock + extra) * 8
checkBits := ne * nblock * 8
p.DataBytes = vtab[v].bytes - ne*nblock
p.CheckBytes = ne * nblock
p.Blocks = nblock
// Make data + checksum pixels.
data := make([]Pixel, dataBits)
for i := range data {
data[i] = Data.Pixel() | OffsetPixel(uint(i))
}
check := make([]Pixel, checkBits)
for i := range check {
check[i] = Check.Pixel() | OffsetPixel(uint(i+dataBits))
}
// Split into blocks.
dataList := make([][]Pixel, nblock)
checkList := make([][]Pixel, nblock)
for i := 0; i < nblock; i++ {
// The last few blocks have an extra data byte (8 pixels).
nd := nde
if i >= nblock-extra {
nd++
}
dataList[i], data = data[0:nd*8], data[nd*8:]
checkList[i], check = check[0:ne*8], check[ne*8:]
}
if len(data) != 0 || len(check) != 0 {
panic("data/check math")
}
// Build up bit sequence, taking first byte of each block,
// then second byte, and so on. Then checksums.
bits := make([]Pixel, dataBits+checkBits)
dst := bits
for i := 0; i < nde+1; i++ {
for _, b := range dataList {
if i*8 < len(b) {
copy(dst, b[i*8:(i+1)*8])
dst = dst[8:]
}
}
}
for i := 0; i < ne; i++ {
for _, b := range checkList {
if i*8 < len(b) {
copy(dst, b[i*8:(i+1)*8])
dst = dst[8:]
}
}
}
if len(dst) != 0 {
panic("dst math")
}
// Sweep up pair of columns,
// then down, assigning to right then left pixel.
// Repeat.
// See Figure 2 of http://www.pclviewer.com/rs2/qrtopology.htm
siz := len(p.Pixel)
rem := make([]Pixel, 7)
for i := range rem {
rem[i] = Extra.Pixel()
}
src := append(bits, rem...)
for x := siz; x > 0; {
for y := siz - 1; y >= 0; y-- {
if p.Pixel[y][x-1].Role() == 0 {
p.Pixel[y][x-1], src = src[0], src[1:]
}
if p.Pixel[y][x-2].Role() == 0 {
p.Pixel[y][x-2], src = src[0], src[1:]
}
}
x -= 2
if x == 7 { // vertical timing strip
x--
}
for y := 0; y < siz; y++ {
if p.Pixel[y][x-1].Role() == 0 {
p.Pixel[y][x-1], src = src[0], src[1:]
}
if p.Pixel[y][x-2].Role() == 0 {
p.Pixel[y][x-2], src = src[0], src[1:]
}
}
x -= 2
}
return nil
}
// mplan edits a version+level-only Plan to add the mask.
func mplan(m Mask, p *Plan) error {
p.Mask = m
for y, row := range p.Pixel {
for x, pix := range row {
if r := pix.Role(); (r == Data || r == Check || r == Extra) && p.Mask.Invert(y, x) {
row[x] ^= Black | Invert
}
}
}
return nil
}
// posBox draws a position (large) box at upper left x, y.
func posBox(m [][]Pixel, x, y int) {
pos := Position.Pixel()
// box
for dy := 0; dy < 7; dy++ {
for dx := 0; dx < 7; dx++ {
p := pos
if dx == 0 || dx == 6 || dy == 0 || dy == 6 || 2 <= dx && dx <= 4 && 2 <= dy && dy <= 4 {
p |= Black
}
m[y+dy][x+dx] = p
}
}
// white border
for dy := -1; dy < 8; dy++ {
if 0 <= y+dy && y+dy < len(m) {
if x > 0 {
m[y+dy][x-1] = pos
}
if x+7 < len(m) {
m[y+dy][x+7] = pos
}
}
}
for dx := -1; dx < 8; dx++ {
if 0 <= x+dx && x+dx < len(m) {
if y > 0 {
m[y-1][x+dx] = pos
}
if y+7 < len(m) {
m[y+7][x+dx] = pos
}
}
}
}
// alignBox draw an alignment (small) box at upper left x, y.
func alignBox(m [][]Pixel, x, y int) {
// box
align := Alignment.Pixel()
for dy := 0; dy < 5; dy++ {
for dx := 0; dx < 5; dx++ {
p := align
if dx == 0 || dx == 4 || dy == 0 || dy == 4 || dx == 2 && dy == 2 {
p |= Black
}
m[y+dy][x+dx] = p
}
}
}

View file

@ -0,0 +1,133 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package coding
import (
"bytes"
"testing"
"github.com/mattermost/rsc/gf256"
"github.com/mattermost/rsc/qr/libqrencode"
)
func test(t *testing.T, v Version, l Level, text ...Encoding) bool {
s := ""
ty := libqrencode.EightBit
switch x := text[0].(type) {
case String:
s = string(x)
case Alpha:
s = string(x)
ty = libqrencode.Alphanumeric
case Num:
s = string(x)
ty = libqrencode.Numeric
}
key, err := libqrencode.Encode(libqrencode.Version(v), libqrencode.Level(l), ty, s)
if err != nil {
t.Errorf("libqrencode.Encode(%v, %v, %d, %#q): %v", v, l, ty, s, err)
return false
}
mask := (^key.Pixel[8][2]&1)<<2 | (key.Pixel[8][3]&1)<<1 | (^key.Pixel[8][4] & 1)
p, err := NewPlan(v, l, Mask(mask))
if err != nil {
t.Errorf("NewPlan(%v, L, %d): %v", v, err, mask)
return false
}
if len(p.Pixel) != len(key.Pixel) {
t.Errorf("%v: NewPlan uses %dx%d, libqrencode uses %dx%d", v, len(p.Pixel), len(p.Pixel), len(key.Pixel), len(key.Pixel))
return false
}
c, err := p.Encode(text...)
if err != nil {
t.Errorf("Encode: %v", err)
return false
}
badpix := 0
Pixel:
for y, prow := range p.Pixel {
for x, pix := range prow {
pix &^= Black
if c.Black(x, y) {
pix |= Black
}
keypix := key.Pixel[y][x]
want := Pixel(0)
switch {
case keypix&libqrencode.Finder != 0:
want = Position.Pixel()
case keypix&libqrencode.Alignment != 0:
want = Alignment.Pixel()
case keypix&libqrencode.Timing != 0:
want = Timing.Pixel()
case keypix&libqrencode.Format != 0:
want = Format.Pixel()
want |= OffsetPixel(pix.Offset()) // sic
want |= pix & Invert
case keypix&libqrencode.PVersion != 0:
want = PVersion.Pixel()
case keypix&libqrencode.DataECC != 0:
if pix.Role() == Check || pix.Role() == Extra {
want = pix.Role().Pixel()
} else {
want = Data.Pixel()
}
want |= OffsetPixel(pix.Offset())
want |= pix & Invert
default:
want = Unused.Pixel()
}
if keypix&libqrencode.Black != 0 {
want |= Black
}
if pix != want {
t.Errorf("%v/%v: Pixel[%d][%d] = %v, want %v %#x", v, mask, y, x, pix, want, keypix)
if badpix++; badpix >= 100 {
t.Errorf("stopping after %d bad pixels", badpix)
break Pixel
}
}
}
}
return badpix == 0
}
var input = []Encoding{
String("hello"),
Num("1"),
Num("12"),
Num("123"),
Alpha("AB"),
Alpha("ABC"),
}
func TestVersion(t *testing.T) {
badvers := 0
Version:
for v := Version(1); v <= 40; v++ {
for l := L; l <= H; l++ {
for _, in := range input {
if !test(t, v, l, in) {
if badvers++; badvers >= 10 {
t.Errorf("stopping after %d bad versions", badvers)
break Version
}
}
}
}
}
}
func TestEncode(t *testing.T) {
data := []byte{0x10, 0x20, 0x0c, 0x56, 0x61, 0x80, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11, 0xec, 0x11}
check := []byte{0xa5, 0x24, 0xd4, 0xc1, 0xed, 0x36, 0xc7, 0x87, 0x2c, 0x55}
rs := gf256.NewRSEncoder(Field, len(check))
out := make([]byte, len(check))
rs.ECC(data, out)
if !bytes.Equal(out, check) {
t.Errorf("have %x want %x", out, check)
}
}

View file

@ -0,0 +1,4 @@
include $(GOROOT)/src/Make.inc
TARG=rsc.googlecode.com/hg/qr/libqrencode
CGOFILES=qrencode.go
include $(GOROOT)/src/Make.pkg

View file

@ -0,0 +1,149 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package libqrencode wraps the C libqrencode library.
// The qr package (in this package's parent directory)
// does not use any C wrapping. This code is here only
// for use during that package's tests.
package libqrencode
/*
#cgo LDFLAGS: -lqrencode
#include <qrencode.h>
*/
import "C"
import (
"fmt"
"image"
"image/color"
"unsafe"
)
type Version int
type Mode int
const (
Numeric Mode = C.QR_MODE_NUM
Alphanumeric Mode = C.QR_MODE_AN
EightBit Mode = C.QR_MODE_8
)
type Level int
const (
L Level = C.QR_ECLEVEL_L
M Level = C.QR_ECLEVEL_M
Q Level = C.QR_ECLEVEL_Q
H Level = C.QR_ECLEVEL_H
)
type Pixel int
const (
Black Pixel = 1 << iota
DataECC
Format
PVersion
Timing
Alignment
Finder
NonData
)
type Code struct {
Version int
Width int
Pixel [][]Pixel
Scale int
}
func (*Code) ColorModel() color.Model {
return color.RGBAModel
}
func (c *Code) Bounds() image.Rectangle {
d := (c.Width + 8) * c.Scale
return image.Rect(0, 0, d, d)
}
var (
white color.Color = color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}
black color.Color = color.RGBA{0x00, 0x00, 0x00, 0xFF}
blue color.Color = color.RGBA{0x00, 0x00, 0x80, 0xFF}
red color.Color = color.RGBA{0xFF, 0x40, 0x40, 0xFF}
yellow color.Color = color.RGBA{0xFF, 0xFF, 0x00, 0xFF}
gray color.Color = color.RGBA{0x80, 0x80, 0x80, 0xFF}
green color.Color = color.RGBA{0x22, 0x8B, 0x22, 0xFF}
)
func (c *Code) At(x, y int) color.Color {
x = x/c.Scale - 4
y = y/c.Scale - 4
if 0 <= x && x < c.Width && 0 <= y && y < c.Width {
switch p := c.Pixel[y][x]; {
case p&Black == 0:
// nothing
case p&DataECC != 0:
return black
case p&Format != 0:
return blue
case p&PVersion != 0:
return red
case p&Timing != 0:
return yellow
case p&Alignment != 0:
return gray
case p&Finder != 0:
return green
}
}
return white
}
type Chunk struct {
Mode Mode
Text string
}
func Encode(version Version, level Level, mode Mode, text string) (*Code, error) {
return EncodeChunk(version, level, Chunk{mode, text})
}
func EncodeChunk(version Version, level Level, chunk ...Chunk) (*Code, error) {
qi, err := C.QRinput_new2(C.int(version), C.QRecLevel(level))
if qi == nil {
return nil, fmt.Errorf("QRinput_new2: %v", err)
}
defer C.QRinput_free(qi)
for _, ch := range chunk {
data := []byte(ch.Text)
n, err := C.QRinput_append(qi, C.QRencodeMode(ch.Mode), C.int(len(data)), (*C.uchar)(&data[0]))
if n < 0 {
return nil, fmt.Errorf("QRinput_append %q: %v", data, err)
}
}
qc, err := C.QRcode_encodeInput(qi)
if qc == nil {
return nil, fmt.Errorf("QRinput_encodeInput: %v", err)
}
c := &Code{
Version: int(qc.version),
Width: int(qc.width),
Scale: 16,
}
pix := make([]Pixel, c.Width*c.Width)
cdat := (*[1000 * 1000]byte)(unsafe.Pointer(qc.data))[:len(pix)]
for i := range pix {
pix[i] = Pixel(cdat[i])
}
c.Pixel = make([][]Pixel, c.Width)
for i := range c.Pixel {
c.Pixel[i] = pix[i*c.Width : (i+1)*c.Width]
}
return c, nil
}

View file

@ -0,0 +1,400 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package qr
// PNG writer for QR codes.
import (
"bytes"
"encoding/binary"
"hash"
"hash/crc32"
)
// PNG returns a PNG image displaying the code.
//
// PNG uses a custom encoder tailored to QR codes.
// Its compressed size is about 2x away from optimal,
// but it runs about 20x faster than calling png.Encode
// on c.Image().
func (c *Code) PNG() []byte {
var p pngWriter
return p.encode(c)
}
type pngWriter struct {
tmp [16]byte
wctmp [4]byte
buf bytes.Buffer
zlib bitWriter
crc hash.Hash32
}
var pngHeader = []byte("\x89PNG\r\n\x1a\n")
func (w *pngWriter) encode(c *Code) []byte {
scale := c.Scale
siz := c.Size
w.buf.Reset()
// Header
w.buf.Write(pngHeader)
// Header block
binary.BigEndian.PutUint32(w.tmp[0:4], uint32((siz+8)*scale))
binary.BigEndian.PutUint32(w.tmp[4:8], uint32((siz+8)*scale))
w.tmp[8] = 1 // 1-bit
w.tmp[9] = 0 // gray
w.tmp[10] = 0
w.tmp[11] = 0
w.tmp[12] = 0
w.writeChunk("IHDR", w.tmp[:13])
// Comment
w.writeChunk("tEXt", comment)
// Data
w.zlib.writeCode(c)
w.writeChunk("IDAT", w.zlib.bytes.Bytes())
// End
w.writeChunk("IEND", nil)
return w.buf.Bytes()
}
var comment = []byte("Software\x00QR-PNG http://qr.swtch.com/")
func (w *pngWriter) writeChunk(name string, data []byte) {
if w.crc == nil {
w.crc = crc32.NewIEEE()
}
binary.BigEndian.PutUint32(w.wctmp[0:4], uint32(len(data)))
w.buf.Write(w.wctmp[0:4])
w.crc.Reset()
copy(w.wctmp[0:4], name)
w.buf.Write(w.wctmp[0:4])
w.crc.Write(w.wctmp[0:4])
w.buf.Write(data)
w.crc.Write(data)
crc := w.crc.Sum32()
binary.BigEndian.PutUint32(w.wctmp[0:4], crc)
w.buf.Write(w.wctmp[0:4])
}
func (b *bitWriter) writeCode(c *Code) {
const ftNone = 0
b.adler32.Reset()
b.bytes.Reset()
b.nbit = 0
scale := c.Scale
siz := c.Size
// zlib header
b.tmp[0] = 0x78
b.tmp[1] = 0
b.tmp[1] += uint8(31 - (uint16(b.tmp[0])<<8+uint16(b.tmp[1]))%31)
b.bytes.Write(b.tmp[0:2])
// Start flate block.
b.writeBits(1, 1, false) // final block
b.writeBits(1, 2, false) // compressed, fixed Huffman tables
// White border.
// First row.
b.byte(ftNone)
n := (scale*(siz+8) + 7) / 8
b.byte(255)
b.repeat(n-1, 1)
// 4*scale rows total.
b.repeat((4*scale-1)*(1+n), 1+n)
for i := 0; i < 4*scale; i++ {
b.adler32.WriteNByte(ftNone, 1)
b.adler32.WriteNByte(255, n)
}
row := make([]byte, 1+n)
for y := 0; y < siz; y++ {
row[0] = ftNone
j := 1
var z uint8
nz := 0
for x := -4; x < siz+4; x++ {
// Raw data.
for i := 0; i < scale; i++ {
z <<= 1
if !c.Black(x, y) {
z |= 1
}
if nz++; nz == 8 {
row[j] = z
j++
nz = 0
}
}
}
if j < len(row) {
row[j] = z
}
for _, z := range row {
b.byte(z)
}
// Scale-1 copies.
b.repeat((scale-1)*(1+n), 1+n)
b.adler32.WriteN(row, scale)
}
// White border.
// First row.
b.byte(ftNone)
b.byte(255)
b.repeat(n-1, 1)
// 4*scale rows total.
b.repeat((4*scale-1)*(1+n), 1+n)
for i := 0; i < 4*scale; i++ {
b.adler32.WriteNByte(ftNone, 1)
b.adler32.WriteNByte(255, n)
}
// End of block.
b.hcode(256)
b.flushBits()
// adler32
binary.BigEndian.PutUint32(b.tmp[0:], b.adler32.Sum32())
b.bytes.Write(b.tmp[0:4])
}
// A bitWriter is a write buffer for bit-oriented data like deflate.
type bitWriter struct {
bytes bytes.Buffer
bit uint32
nbit uint
tmp [4]byte
adler32 adigest
}
func (b *bitWriter) writeBits(bit uint32, nbit uint, rev bool) {
// reverse, for huffman codes
if rev {
br := uint32(0)
for i := uint(0); i < nbit; i++ {
br |= ((bit >> i) & 1) << (nbit - 1 - i)
}
bit = br
}
b.bit |= bit << b.nbit
b.nbit += nbit
for b.nbit >= 8 {
b.bytes.WriteByte(byte(b.bit))
b.bit >>= 8
b.nbit -= 8
}
}
func (b *bitWriter) flushBits() {
if b.nbit > 0 {
b.bytes.WriteByte(byte(b.bit))
b.nbit = 0
b.bit = 0
}
}
func (b *bitWriter) hcode(v int) {
/*
Lit Value Bits Codes
--------- ---- -----
0 - 143 8 00110000 through
10111111
144 - 255 9 110010000 through
111111111
256 - 279 7 0000000 through
0010111
280 - 287 8 11000000 through
11000111
*/
switch {
case v <= 143:
b.writeBits(uint32(v)+0x30, 8, true)
case v <= 255:
b.writeBits(uint32(v-144)+0x190, 9, true)
case v <= 279:
b.writeBits(uint32(v-256)+0, 7, true)
case v <= 287:
b.writeBits(uint32(v-280)+0xc0, 8, true)
default:
panic("invalid hcode")
}
}
func (b *bitWriter) byte(x byte) {
b.hcode(int(x))
}
func (b *bitWriter) codex(c int, val int, nx uint) {
b.hcode(c + val>>nx)
b.writeBits(uint32(val)&(1<<nx-1), nx, false)
}
func (b *bitWriter) repeat(n, d int) {
for ; n >= 258+3; n -= 258 {
b.repeat1(258, d)
}
if n > 258 {
// 258 < n < 258+3
b.repeat1(10, d)
b.repeat1(n-10, d)
return
}
if n < 3 {
panic("invalid flate repeat")
}
b.repeat1(n, d)
}
func (b *bitWriter) repeat1(n, d int) {
/*
Extra Extra Extra
Code Bits Length(s) Code Bits Lengths Code Bits Length(s)
---- ---- ------ ---- ---- ------- ---- ---- -------
257 0 3 267 1 15,16 277 4 67-82
258 0 4 268 1 17,18 278 4 83-98
259 0 5 269 2 19-22 279 4 99-114
260 0 6 270 2 23-26 280 4 115-130
261 0 7 271 2 27-30 281 5 131-162
262 0 8 272 2 31-34 282 5 163-194
263 0 9 273 3 35-42 283 5 195-226
264 0 10 274 3 43-50 284 5 227-257
265 1 11,12 275 3 51-58 285 0 258
266 1 13,14 276 3 59-66
*/
switch {
case n <= 10:
b.codex(257, n-3, 0)
case n <= 18:
b.codex(265, n-11, 1)
case n <= 34:
b.codex(269, n-19, 2)
case n <= 66:
b.codex(273, n-35, 3)
case n <= 130:
b.codex(277, n-67, 4)
case n <= 257:
b.codex(281, n-131, 5)
case n == 258:
b.hcode(285)
default:
panic("invalid repeat length")
}
/*
Extra Extra Extra
Code Bits Dist Code Bits Dist Code Bits Distance
---- ---- ---- ---- ---- ------ ---- ---- --------
0 0 1 10 4 33-48 20 9 1025-1536
1 0 2 11 4 49-64 21 9 1537-2048
2 0 3 12 5 65-96 22 10 2049-3072
3 0 4 13 5 97-128 23 10 3073-4096
4 1 5,6 14 6 129-192 24 11 4097-6144
5 1 7,8 15 6 193-256 25 11 6145-8192
6 2 9-12 16 7 257-384 26 12 8193-12288
7 2 13-16 17 7 385-512 27 12 12289-16384
8 3 17-24 18 8 513-768 28 13 16385-24576
9 3 25-32 19 8 769-1024 29 13 24577-32768
*/
if d <= 4 {
b.writeBits(uint32(d-1), 5, true)
} else if d <= 32768 {
nbit := uint(16)
for d <= 1<<(nbit-1) {
nbit--
}
v := uint32(d - 1)
v &^= 1 << (nbit - 1) // top bit is implicit
code := uint32(2*nbit - 2) // second bit is low bit of code
code |= v >> (nbit - 2)
v &^= 1 << (nbit - 2)
b.writeBits(code, 5, true)
// rest of bits follow
b.writeBits(uint32(v), nbit-2, false)
} else {
panic("invalid repeat distance")
}
}
func (b *bitWriter) run(v byte, n int) {
if n == 0 {
return
}
b.byte(v)
if n-1 < 3 {
for i := 0; i < n-1; i++ {
b.byte(v)
}
} else {
b.repeat(n-1, 1)
}
}
type adigest struct {
a, b uint32
}
func (d *adigest) Reset() { d.a, d.b = 1, 0 }
const amod = 65521
func aupdate(a, b uint32, pi byte, n int) (aa, bb uint32) {
// TODO(rsc): 6g doesn't do magic multiplies for b %= amod,
// only for b = b%amod.
// invariant: a, b < amod
if pi == 0 {
b += uint32(n%amod) * a
b = b % amod
return a, b
}
// n times:
// a += pi
// b += a
// is same as
// b += n*a + n*(n+1)/2*pi
// a += n*pi
m := uint32(n)
b += (m % amod) * a
b = b % amod
b += (m * (m + 1) / 2) % amod * uint32(pi)
b = b % amod
a += (m % amod) * uint32(pi)
a = a % amod
return a, b
}
func afinish(a, b uint32) uint32 {
return b<<16 | a
}
func (d *adigest) WriteN(p []byte, n int) {
for i := 0; i < n; i++ {
for _, pi := range p {
d.a, d.b = aupdate(d.a, d.b, pi, 1)
}
}
}
func (d *adigest) WriteNByte(pi byte, n int) {
d.a, d.b = aupdate(d.a, d.b, pi, n)
}
func (d *adigest) Sum32() uint32 { return afinish(d.a, d.b) }

View file

@ -0,0 +1,73 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package qr
import (
"bytes"
"image"
"image/color"
"image/png"
"io/ioutil"
"testing"
)
func TestPNG(t *testing.T) {
c, err := Encode("hello, world", L)
if err != nil {
t.Fatal(err)
}
pngdat := c.PNG()
if true {
ioutil.WriteFile("x.png", pngdat, 0666)
}
m, err := png.Decode(bytes.NewBuffer(pngdat))
if err != nil {
t.Fatal(err)
}
gm := m.(*image.Gray)
scale := c.Scale
siz := c.Size
nbad := 0
for y := 0; y < scale*(8+siz); y++ {
for x := 0; x < scale*(8+siz); x++ {
v := byte(255)
if c.Black(x/scale-4, y/scale-4) {
v = 0
}
if gv := gm.At(x, y).(color.Gray).Y; gv != v {
t.Errorf("%d,%d = %d, want %d", x, y, gv, v)
if nbad++; nbad >= 20 {
t.Fatalf("too many bad pixels")
}
}
}
}
}
func BenchmarkPNG(b *testing.B) {
c, err := Encode("0123456789012345678901234567890123456789", L)
if err != nil {
panic(err)
}
var bytes []byte
for i := 0; i < b.N; i++ {
bytes = c.PNG()
}
b.SetBytes(int64(len(bytes)))
}
func BenchmarkImagePNG(b *testing.B) {
c, err := Encode("0123456789012345678901234567890123456789", L)
if err != nil {
panic(err)
}
var buf bytes.Buffer
for i := 0; i < b.N; i++ {
buf.Reset()
png.Encode(&buf, c.Image())
}
b.SetBytes(int64(buf.Len()))
}

View file

@ -0,0 +1,116 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package qr encodes QR codes.
*/
package qr
import (
"errors"
"image"
"image/color"
"github.com/mattermost/rsc/qr/coding"
)
// A Level denotes a QR error correction level.
// From least to most tolerant of errors, they are L, M, Q, H.
type Level int
const (
L Level = iota // 20% redundant
M // 38% redundant
Q // 55% redundant
H // 65% redundant
)
// Encode returns an encoding of text at the given error correction level.
func Encode(text string, level Level) (*Code, error) {
// Pick data encoding, smallest first.
// We could split the string and use different encodings
// but that seems like overkill for now.
var enc coding.Encoding
switch {
case coding.Num(text).Check() == nil:
enc = coding.Num(text)
case coding.Alpha(text).Check() == nil:
enc = coding.Alpha(text)
default:
enc = coding.String(text)
}
// Pick size.
l := coding.Level(level)
var v coding.Version
for v = coding.MinVersion; ; v++ {
if v > coding.MaxVersion {
return nil, errors.New("text too long to encode as QR")
}
if enc.Bits(v) <= v.DataBytes(l)*8 {
break
}
}
// Build and execute plan.
p, err := coding.NewPlan(v, l, 0)
if err != nil {
return nil, err
}
cc, err := p.Encode(enc)
if err != nil {
return nil, err
}
// TODO: Pick appropriate mask.
return &Code{cc.Bitmap, cc.Size, cc.Stride, 8}, nil
}
// A Code is a square pixel grid.
// It implements image.Image and direct PNG encoding.
type Code struct {
Bitmap []byte // 1 is black, 0 is white
Size int // number of pixels on a side
Stride int // number of bytes per row
Scale int // number of image pixels per QR pixel
}
// Black returns true if the pixel at (x,y) is black.
func (c *Code) Black(x, y int) bool {
return 0 <= x && x < c.Size && 0 <= y && y < c.Size &&
c.Bitmap[y*c.Stride+x/8]&(1<<uint(7-x&7)) != 0
}
// Image returns an Image displaying the code.
func (c *Code) Image() image.Image {
return &codeImage{c}
}
// codeImage implements image.Image
type codeImage struct {
*Code
}
var (
whiteColor color.Color = color.Gray{0xFF}
blackColor color.Color = color.Gray{0x00}
)
func (c *codeImage) Bounds() image.Rectangle {
d := (c.Size + 8) * c.Scale
return image.Rect(0, 0, d, d)
}
func (c *codeImage) At(x, y int) color.Color {
if c.Black(x, y) {
return blackColor
}
return whiteColor
}
func (c *codeImage) ColorModel() color.Model {
return color.GrayModel
}

View file

@ -0,0 +1,506 @@
package web
import (
"bytes"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"net/http"
"strconv"
"strings"
"code.google.com/p/freetype-go/freetype"
"github.com/mattermost/rsc/appfs/fs"
"github.com/mattermost/rsc/qr"
"github.com/mattermost/rsc/qr/coding"
)
func makeImage(req *http.Request, caption, font string, pt, size, border, scale int, f func(x, y int) uint32) *image.RGBA {
d := (size + 2*border) * scale
csize := 0
if caption != "" {
if pt == 0 {
pt = 11
}
csize = pt * 2
}
c := image.NewRGBA(image.Rect(0, 0, d, d+csize))
// white
u := &image.Uniform{C: color.White}
draw.Draw(c, c.Bounds(), u, image.ZP, draw.Src)
for y := 0; y < size; y++ {
for x := 0; x < size; x++ {
r := image.Rect((x+border)*scale, (y+border)*scale, (x+border+1)*scale, (y+border+1)*scale)
rgba := f(x, y)
u.C = color.RGBA{byte(rgba >> 24), byte(rgba >> 16), byte(rgba >> 8), byte(rgba)}
draw.Draw(c, r, u, image.ZP, draw.Src)
}
}
if csize != 0 {
if font == "" {
font = "data/luxisr.ttf"
}
ctxt := fs.NewContext(req)
dat, _, err := ctxt.Read(font)
if err != nil {
panic(err)
}
tfont, err := freetype.ParseFont(dat)
if err != nil {
panic(err)
}
ft := freetype.NewContext()
ft.SetDst(c)
ft.SetDPI(100)
ft.SetFont(tfont)
ft.SetFontSize(float64(pt))
ft.SetSrc(image.NewUniform(color.Black))
ft.SetClip(image.Rect(0, 0, 0, 0))
wid, err := ft.DrawString(caption, freetype.Pt(0, 0))
if err != nil {
panic(err)
}
p := freetype.Pt(d, d+3*pt/2)
p.X -= wid.X
p.X /= 2
ft.SetClip(c.Bounds())
ft.DrawString(caption, p)
}
return c
}
func makeFrame(req *http.Request, font string, pt, vers, l, scale, dots int) image.Image {
lev := coding.Level(l)
p, err := coding.NewPlan(coding.Version(vers), lev, 0)
if err != nil {
panic(err)
}
nd := p.DataBytes / p.Blocks
nc := p.CheckBytes / p.Blocks
extra := p.DataBytes - nd*p.Blocks
cap := fmt.Sprintf("QR v%d, %s", vers, lev)
if dots > 0 {
cap = fmt.Sprintf("QR v%d order, from bottom right", vers)
}
m := makeImage(req, cap, font, pt, len(p.Pixel), 0, scale, func(x, y int) uint32 {
pix := p.Pixel[y][x]
switch pix.Role() {
case coding.Data:
if dots > 0 {
return 0xffffffff
}
off := int(pix.Offset() / 8)
nd := nd
var i int
for i = 0; i < p.Blocks; i++ {
if i == extra {
nd++
}
if off < nd {
break
}
off -= nd
}
return blockColors[i%len(blockColors)]
case coding.Check:
if dots > 0 {
return 0xffffffff
}
i := (int(pix.Offset()/8) - p.DataBytes) / nc
return dark(blockColors[i%len(blockColors)])
}
if pix&coding.Black != 0 {
return 0x000000ff
}
return 0xffffffff
})
if dots > 0 {
b := m.Bounds()
for y := 0; y <= len(p.Pixel); y++ {
for x := 0; x < b.Dx(); x++ {
m.SetRGBA(x, y*scale-(y/len(p.Pixel)), color.RGBA{127, 127, 127, 255})
}
}
for x := 0; x <= len(p.Pixel); x++ {
for y := 0; y < b.Dx(); y++ {
m.SetRGBA(x*scale-(x/len(p.Pixel)), y, color.RGBA{127, 127, 127, 255})
}
}
order := make([]image.Point, (p.DataBytes+p.CheckBytes)*8+1)
for y, row := range p.Pixel {
for x, pix := range row {
if r := pix.Role(); r != coding.Data && r != coding.Check {
continue
}
// draw.Draw(m, m.Bounds().Add(image.Pt(x*scale, y*scale)), dot, image.ZP, draw.Over)
order[pix.Offset()] = image.Point{x*scale + scale/2, y*scale + scale/2}
}
}
for mode := 0; mode < 2; mode++ {
for i, p := range order {
q := order[i+1]
if q.X == 0 {
break
}
line(m, p, q, mode)
}
}
}
return m
}
func line(m *image.RGBA, p, q image.Point, mode int) {
x := 0
y := 0
dx := q.X - p.X
dy := q.Y - p.Y
xsign := +1
ysign := +1
if dx < 0 {
xsign = -1
dx = -dx
}
if dy < 0 {
ysign = -1
dy = -dy
}
pt := func() {
switch mode {
case 0:
for dx := -2; dx <= 2; dx++ {
for dy := -2; dy <= 2; dy++ {
if dy*dx <= -4 || dy*dx >= 4 {
continue
}
m.SetRGBA(p.X+x*xsign+dx, p.Y+y*ysign+dy, color.RGBA{255, 192, 192, 255})
}
}
case 1:
m.SetRGBA(p.X+x*xsign, p.Y+y*ysign, color.RGBA{128, 0, 0, 255})
}
}
if dx > dy {
for x < dx || y < dy {
pt()
x++
if float64(x)*float64(dy)/float64(dx)-float64(y) > 0.5 {
y++
}
}
} else {
for x < dx || y < dy {
pt()
y++
if float64(y)*float64(dx)/float64(dy)-float64(x) > 0.5 {
x++
}
}
}
pt()
}
func pngEncode(c image.Image) []byte {
var b bytes.Buffer
png.Encode(&b, c)
return b.Bytes()
}
// Frame handles a request for a single QR frame.
func Frame(w http.ResponseWriter, req *http.Request) {
arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x }
v := arg("v")
scale := arg("scale")
if scale == 0 {
scale = 8
}
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Write(pngEncode(makeFrame(req, req.FormValue("font"), arg("pt"), v, arg("l"), scale, arg("dots"))))
}
// Frames handles a request for multiple QR frames.
func Frames(w http.ResponseWriter, req *http.Request) {
vs := strings.Split(req.FormValue("v"), ",")
arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x }
scale := arg("scale")
if scale == 0 {
scale = 8
}
font := req.FormValue("font")
pt := arg("pt")
dots := arg("dots")
var images []image.Image
l := arg("l")
for _, v := range vs {
l := l
if i := strings.Index(v, "."); i >= 0 {
l, _ = strconv.Atoi(v[i+1:])
v = v[:i]
}
vv, _ := strconv.Atoi(v)
images = append(images, makeFrame(req, font, pt, vv, l, scale, dots))
}
b := images[len(images)-1].Bounds()
dx := arg("dx")
if dx == 0 {
dx = b.Dx()
}
x, y := 0, 0
xmax := 0
sep := arg("sep")
if sep == 0 {
sep = 10
}
var points []image.Point
for i, m := range images {
if x > 0 {
x += sep
}
if x > 0 && x+m.Bounds().Dx() > dx {
y += sep + images[i-1].Bounds().Dy()
x = 0
}
points = append(points, image.Point{x, y})
x += m.Bounds().Dx()
if x > xmax {
xmax = x
}
}
c := image.NewRGBA(image.Rect(0, 0, xmax, y+b.Dy()))
for i, m := range images {
draw.Draw(c, c.Bounds().Add(points[i]), m, image.ZP, draw.Src)
}
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Write(pngEncode(c))
}
// Mask handles a request for a single QR mask.
func Mask(w http.ResponseWriter, req *http.Request) {
arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x }
v := arg("v")
m := arg("m")
scale := arg("scale")
if scale == 0 {
scale = 8
}
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Write(pngEncode(makeMask(req, req.FormValue("font"), arg("pt"), v, m, scale)))
}
// Masks handles a request for multiple QR masks.
func Masks(w http.ResponseWriter, req *http.Request) {
arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x }
v := arg("v")
scale := arg("scale")
if scale == 0 {
scale = 8
}
font := req.FormValue("font")
pt := arg("pt")
var mm []image.Image
for m := 0; m < 8; m++ {
mm = append(mm, makeMask(req, font, pt, v, m, scale))
}
dx := mm[0].Bounds().Dx()
dy := mm[0].Bounds().Dy()
sep := arg("sep")
if sep == 0 {
sep = 10
}
c := image.NewRGBA(image.Rect(0, 0, (dx+sep)*4-sep, (dy+sep)*2-sep))
for m := 0; m < 8; m++ {
x := (m % 4) * (dx + sep)
y := (m / 4) * (dy + sep)
draw.Draw(c, c.Bounds().Add(image.Pt(x, y)), mm[m], image.ZP, draw.Src)
}
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Write(pngEncode(c))
}
var maskName = []string{
"(x+y) % 2",
"y % 2",
"x % 3",
"(x+y) % 3",
"(y/2 + x/3) % 2",
"xy%2 + xy%3",
"(xy%2 + xy%3) % 2",
"(xy%3 + (x+y)%2) % 2",
}
func makeMask(req *http.Request, font string, pt int, vers, mask, scale int) image.Image {
p, err := coding.NewPlan(coding.Version(vers), coding.L, coding.Mask(mask))
if err != nil {
panic(err)
}
m := makeImage(req, maskName[mask], font, pt, len(p.Pixel), 0, scale, func(x, y int) uint32 {
pix := p.Pixel[y][x]
switch pix.Role() {
case coding.Data, coding.Check:
if pix&coding.Invert != 0 {
return 0x000000ff
}
}
return 0xffffffff
})
return m
}
var blockColors = []uint32{
0x7777ffff,
0xffff77ff,
0xff7777ff,
0x77ffffff,
0x1e90ffff,
0xffffe0ff,
0x8b6969ff,
0x77ff77ff,
0x9b30ffff,
0x00bfffff,
0x90e890ff,
0xfff68fff,
0xffec8bff,
0xffa07aff,
0xffa54fff,
0xeee8aaff,
0x98fb98ff,
0xbfbfbfff,
0x54ff9fff,
0xffaeb9ff,
0xb23aeeff,
0xbbffffff,
0x7fffd4ff,
0xff7a7aff,
0x00007fff,
}
func dark(x uint32) uint32 {
r, g, b, a := byte(x>>24), byte(x>>16), byte(x>>8), byte(x)
r = r/2 + r/4
g = g/2 + g/4
b = b/2 + b/4
return uint32(r)<<24 | uint32(g)<<16 | uint32(b)<<8 | uint32(a)
}
func clamp(x int) byte {
if x < 0 {
return 0
}
if x > 255 {
return 255
}
return byte(x)
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
// Arrow handles a request for an arrow pointing in a given direction.
func Arrow(w http.ResponseWriter, req *http.Request) {
arg := func(s string) int { x, _ := strconv.Atoi(req.FormValue(s)); return x }
dir := arg("dir")
size := arg("size")
if size == 0 {
size = 50
}
del := size / 10
m := image.NewRGBA(image.Rect(0, 0, size, size))
if dir == 4 {
draw.Draw(m, m.Bounds(), image.Black, image.ZP, draw.Src)
draw.Draw(m, image.Rect(5, 5, size-5, size-5), image.White, image.ZP, draw.Src)
}
pt := func(x, y int, c color.RGBA) {
switch dir {
case 0:
m.SetRGBA(x, y, c)
case 1:
m.SetRGBA(y, size-1-x, c)
case 2:
m.SetRGBA(size-1-x, size-1-y, c)
case 3:
m.SetRGBA(size-1-y, x, c)
}
}
for y := 0; y < size/2; y++ {
for x := 0; x < del && x < y; x++ {
pt(x, y, color.RGBA{0, 0, 0, 255})
}
for x := del; x < y-del; x++ {
pt(x, y, color.RGBA{128, 128, 255, 255})
}
for x := max(y-del, 0); x <= y; x++ {
pt(x, y, color.RGBA{0, 0, 0, 255})
}
}
for y := size / 2; y < size; y++ {
for x := 0; x < del && x < size-1-y; x++ {
pt(x, y, color.RGBA{0, 0, 0, 255})
}
for x := del; x < size-1-y-del; x++ {
pt(x, y, color.RGBA{128, 128, 192, 255})
}
for x := max(size-1-y-del, 0); x <= size-1-y; x++ {
pt(x, y, color.RGBA{0, 0, 0, 255})
}
}
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Write(pngEncode(m))
}
// Encode encodes a string using the given version, level, and mask.
func Encode(w http.ResponseWriter, req *http.Request) {
val := func(s string) int {
v, _ := strconv.Atoi(req.FormValue(s))
return v
}
l := coding.Level(val("l"))
v := coding.Version(val("v"))
enc := coding.String(req.FormValue("t"))
m := coding.Mask(val("m"))
p, err := coding.NewPlan(v, l, m)
if err != nil {
panic(err)
}
cc, err := p.Encode(enc)
if err != nil {
panic(err)
}
c := &qr.Code{Bitmap: cc.Bitmap, Size: cc.Size, Stride: cc.Stride, Scale: 8}
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Write(c.PNG())
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,152 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package resize
import (
"image"
"image/color"
)
// average convert the sums to averages and returns the result.
func average(sum []uint64, w, h int, n uint64) *image.RGBA {
ret := image.NewRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
index := 4 * (y*w + x)
pix := ret.Pix[y*ret.Stride+x*4:]
pix[0] = uint8(sum[index+0] / n)
pix[1] = uint8(sum[index+1] / n)
pix[2] = uint8(sum[index+2] / n)
pix[3] = uint8(sum[index+3] / n)
}
}
return ret
}
// ResizeRGBA returns a scaled copy of the RGBA image slice r of m.
// The returned image has width w and height h.
func ResizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) *image.RGBA {
ww, hh := uint64(w), uint64(h)
dx, dy := uint64(r.Dx()), uint64(r.Dy())
// See comment in Resize.
n, sum := dx*dy, make([]uint64, 4*w*h)
for y := r.Min.Y; y < r.Max.Y; y++ {
pix := m.Pix[(y-r.Min.Y)*m.Stride:]
for x := r.Min.X; x < r.Max.X; x++ {
// Get the source pixel.
p := pix[(x-r.Min.X)*4:]
r64 := uint64(p[0])
g64 := uint64(p[1])
b64 := uint64(p[2])
a64 := uint64(p[3])
// Spread the source pixel over 1 or more destination rows.
py := uint64(y) * hh
for remy := hh; remy > 0; {
qy := dy - (py % dy)
if qy > remy {
qy = remy
}
// Spread the source pixel over 1 or more destination columns.
px := uint64(x) * ww
index := 4 * ((py/dy)*ww + (px / dx))
for remx := ww; remx > 0; {
qx := dx - (px % dx)
if qx > remx {
qx = remx
}
qxy := qx * qy
sum[index+0] += r64 * qxy
sum[index+1] += g64 * qxy
sum[index+2] += b64 * qxy
sum[index+3] += a64 * qxy
index += 4
px += qx
remx -= qx
}
py += qy
remy -= qy
}
}
}
return average(sum, w, h, n)
}
// ResizeNRGBA returns a scaled copy of the RGBA image slice r of m.
// The returned image has width w and height h.
func ResizeNRGBA(m *image.NRGBA, r image.Rectangle, w, h int) *image.RGBA {
ww, hh := uint64(w), uint64(h)
dx, dy := uint64(r.Dx()), uint64(r.Dy())
// See comment in Resize.
n, sum := dx*dy, make([]uint64, 4*w*h)
for y := r.Min.Y; y < r.Max.Y; y++ {
pix := m.Pix[(y-r.Min.Y)*m.Stride:]
for x := r.Min.X; x < r.Max.X; x++ {
// Get the source pixel.
p := pix[(x-r.Min.X)*4:]
r64 := uint64(p[0])
g64 := uint64(p[1])
b64 := uint64(p[2])
a64 := uint64(p[3])
r64 = (r64 * a64) / 255
g64 = (g64 * a64) / 255
b64 = (b64 * a64) / 255
// Spread the source pixel over 1 or more destination rows.
py := uint64(y) * hh
for remy := hh; remy > 0; {
qy := dy - (py % dy)
if qy > remy {
qy = remy
}
// Spread the source pixel over 1 or more destination columns.
px := uint64(x) * ww
index := 4 * ((py/dy)*ww + (px / dx))
for remx := ww; remx > 0; {
qx := dx - (px % dx)
if qx > remx {
qx = remx
}
qxy := qx * qy
sum[index+0] += r64 * qxy
sum[index+1] += g64 * qxy
sum[index+2] += b64 * qxy
sum[index+3] += a64 * qxy
index += 4
px += qx
remx -= qx
}
py += qy
remy -= qy
}
}
}
return average(sum, w, h, n)
}
// Resample returns a resampled copy of the image slice r of m.
// The returned image has width w and height h.
func Resample(m image.Image, r image.Rectangle, w, h int) *image.RGBA {
if w < 0 || h < 0 {
return nil
}
if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 {
return image.NewRGBA(image.Rect(0, 0, w, h))
}
curw, curh := r.Dx(), r.Dy()
img := image.NewRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
// Get a source pixel.
subx := x * curw / w
suby := y * curh / h
r32, g32, b32, a32 := m.At(subx, suby).RGBA()
r := uint8(r32 >> 8)
g := uint8(g32 >> 8)
b := uint8(b32 >> 8)
a := uint8(a32 >> 8)
img.SetRGBA(x, y, color.RGBA{r, g, b, a})
}
}
return img
}

View file

@ -53,6 +53,9 @@ func InitUser(r *mux.Router) {
sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST")
sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST")
sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST")
sr.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST")
sr.Handle("/generate_mfa_qr", ApiUserRequired(generateMfaQrCode)).Methods("GET")
sr.Handle("/update_mfa", ApiUserRequired(updateMfa)).Methods("POST")
sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST")
@ -405,13 +408,14 @@ func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDispl
}()
}
func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, deviceId string) *model.User {
func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, mfaToken, deviceId string) *model.User {
if result := <-Srv.Store.User().Get(userId); result.Err != nil {
c.Err = result.Err
return nil
} else {
user := result.Data.(*model.User)
if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) {
if authenticateUserPasswordAndToken(c, user, password, mfaToken) {
Login(c, w, r, user, deviceId)
return user
}
@ -420,7 +424,7 @@ func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, passw
return nil
}
func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, deviceId string) *model.User {
func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, mfaToken, deviceId string) *model.User {
var team *model.Team
if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
@ -443,7 +447,7 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
return nil
}
if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) {
if authenticateUserPasswordAndToken(c, user, password, mfaToken) {
Login(c, w, r, user, deviceId)
return user
}
@ -452,7 +456,7 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
return nil
}
func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, deviceId string) *model.User {
func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, mfaToken, deviceId string) *model.User {
var team *model.Team
if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
@ -475,7 +479,7 @@ func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, usernam
return nil
}
if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) {
if authenticateUserPasswordAndToken(c, user, password, mfaToken) {
Login(c, w, r, user, deviceId)
return user
}
@ -518,6 +522,10 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st
}
}
func authenticateUserPasswordAndToken(c *Context, user *model.User, password string, token string) bool {
return checkUserLoginAttempts(c, user) && checkUserMfa(c, user, token) && checkUserPassword(c, user, password)
}
func checkUserLoginAttempts(c *Context, user *model.User) bool {
if user.FailedAttempts >= utils.Cfg.ServiceSettings.MaximumLoginAttempts {
c.LogAuditWithUserId(user.Id, "fail")
@ -530,7 +538,6 @@ func checkUserLoginAttempts(c *Context, user *model.User) bool {
}
func checkUserPassword(c *Context, user *model.User, password string) bool {
if !model.ComparePassword(user.Password, password) {
c.LogAuditWithUserId(user.Id, "fail")
c.Err = model.NewLocAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id)
@ -548,7 +555,29 @@ func checkUserPassword(c *Context, user *model.User, password string) bool {
return true
}
}
func checkUserMfa(c *Context, user *model.User, token string) bool {
if !user.MfaActive || !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication {
return true
}
mfaInterface := einterfaces.GetMfaInterface()
if mfaInterface == nil {
c.Err = model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.not_available.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return false
}
if ok, err := mfaInterface.ValidateToken(user.MfaSecret, token); err != nil {
c.Err = err
return false
} else if !ok {
c.Err = model.NewLocAppError("checkUserMfa", "api.user.check_user_mfa.bad_code.app_error", nil, "")
return false
} else {
return true
}
}
// User MUST be validated before calling Login
@ -660,11 +689,11 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
var user *model.User
if len(props["id"]) != 0 {
user = LoginById(c, w, r, props["id"], props["password"], props["device_id"])
user = LoginById(c, w, r, props["id"], props["password"], props["token"], props["device_id"])
} else if len(props["email"]) != 0 && len(props["name"]) != 0 {
user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"])
user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["token"], props["device_id"])
} else if len(props["username"]) != 0 && len(props["name"]) != 0 {
user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["device_id"])
user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["token"], props["device_id"])
} else {
c.Err = model.NewLocAppError("login", "api.user.login.not_provided.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
@ -695,6 +724,7 @@ func loginLdap(c *Context, w http.ResponseWriter, r *http.Request) {
password := props["password"]
id := props["id"]
teamName := props["teamName"]
mfaToken := props["token"]
if len(password) == 0 {
c.Err = model.NewLocAppError("loginLdap", "api.user.login_ldap.blank_pwd.app_error", nil, "")
@ -735,6 +765,10 @@ func loginLdap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if !checkUserMfa(c, user, mfaToken) {
return
}
// User is authenticated at this point
Login(c, w, r, user, props["device_id"])
@ -2487,3 +2521,146 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
}
func generateMfaQrCode(c *Context, w http.ResponseWriter, r *http.Request) {
uchan := Srv.Store.User().Get(c.Session.UserId)
tchan := Srv.Store.Team().Get(c.Session.TeamId)
var user *model.User
if result := <-uchan; result.Err != nil {
c.Err = result.Err
return
} else {
user = result.Data.(*model.User)
}
var team *model.Team
if result := <-tchan; result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
mfaInterface := einterfaces.GetMfaInterface()
if mfaInterface == nil {
c.Err = model.NewLocAppError("generateMfaQrCode", "api.user.generate_mfa_qr.not_available.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
img, err := mfaInterface.GenerateQrCode(team, user)
if err != nil {
c.Err = err
return
}
w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
w.Write(img)
}
func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJson(r.Body)
activate, ok := props["activate"].(bool)
if !ok {
c.SetInvalidParam("updateMfa", "activate")
return
}
token := ""
if activate {
token = props["token"].(string)
if len(token) == 0 {
c.SetInvalidParam("updateMfa", "token")
return
}
}
mfaInterface := einterfaces.GetMfaInterface()
if mfaInterface == nil {
c.Err = model.NewLocAppError("generateMfaQrCode", "api.user.update_mfa.not_available.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if activate {
var user *model.User
if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
c.Err = result.Err
return
} else {
user = result.Data.(*model.User)
}
if err := mfaInterface.Activate(user, token); err != nil {
c.Err = err
return
}
} else {
if err := mfaInterface.Deactivate(c.Session.UserId); err != nil {
c.Err = err
return
}
}
rdata := map[string]string{}
rdata["status"] = "ok"
w.Write([]byte(model.MapToJson(rdata)))
}
func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication {
rdata := map[string]string{}
rdata["mfa_required"] = "false"
w.Write([]byte(model.MapToJson(rdata)))
return
}
props := model.MapFromJson(r.Body)
method := props["method"]
if method != model.USER_AUTH_SERVICE_EMAIL &&
method != model.USER_AUTH_SERVICE_USERNAME &&
method != model.USER_AUTH_SERVICE_LDAP {
c.SetInvalidParam("checkMfa", "method")
return
}
teamName := props["team_name"]
if len(teamName) == 0 {
c.SetInvalidParam("checkMfa", "team_name")
return
}
loginId := props["login_id"]
if len(loginId) == 0 {
c.SetInvalidParam("checkMfa", "login_id")
return
}
var team *model.Team
if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
var uchan store.StoreChannel
if method == model.USER_AUTH_SERVICE_EMAIL {
uchan = Srv.Store.User().GetByEmail(team.Id, loginId)
} else if method == model.USER_AUTH_SERVICE_USERNAME {
uchan = Srv.Store.User().GetByUsername(team.Id, loginId)
} else if method == model.USER_AUTH_SERVICE_LDAP {
uchan = Srv.Store.User().GetByAuth(team.Id, loginId, model.USER_AUTH_SERVICE_LDAP)
}
rdata := map[string]string{}
if result := <-uchan; result.Err != nil {
rdata["mfa_required"] = "false"
} else {
rdata["mfa_required"] = strconv.FormatBool(result.Data.(*model.User).MfaActive)
}
w.Write([]byte(model.MapToJson(rdata)))
}

View file

@ -1411,3 +1411,79 @@ func TestMeLoggedIn(t *testing.T) {
}
}
}
func TestGenerateMfaQrCode(t *testing.T) {
Setup()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
ruser, _ := Client.CreateUser(&user, "")
store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
Client.Logout()
if _, err := Client.GenerateMfaQrCode(); err == nil {
t.Fatal("should have failed - not logged in")
}
Client.LoginByEmail(team.Name, user.Email, user.Password)
if _, err := Client.GenerateMfaQrCode(); err == nil {
t.Fatal("should have failed - not licensed")
}
// need to add more test cases when license and config can be configured for tests
}
func TestUpdateMfa(t *testing.T) {
Setup()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
ruser, _ := Client.CreateUser(&user, "")
store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
Client.Logout()
if _, err := Client.UpdateMfa(true, "123456"); err == nil {
t.Fatal("should have failed - not logged in")
}
Client.LoginByEmail(team.Name, user.Email, user.Password)
if _, err := Client.UpdateMfa(true, ""); err == nil {
t.Fatal("should have failed - no token")
}
if _, err := Client.UpdateMfa(true, "123456"); err == nil {
t.Fatal("should have failed - not licensed")
}
// need to add more test cases when license and config can be configured for tests
}
func TestCheckMfa(t *testing.T) {
Setup()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
ruser, _ := Client.CreateUser(&user, "")
store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
if result, err := Client.CheckMfa(model.USER_AUTH_SERVICE_EMAIL, team.Name, user.Email); err != nil {
t.Fatal(err)
} else {
resp := result.Data.(map[string]string)
if resp["mfa_required"] != "false" {
t.Fatal("mfa should not be required")
}
}
// need to add more test cases when license and config can be configured for tests
}

View file

@ -15,6 +15,7 @@
"EnableDeveloper": false,
"EnableSecurityFixAlert": true,
"EnableInsecureOutgoingConnections": false,
"EnableMultifactorAuthentication": false,
"AllowCorsFrom": "",
"SessionLengthWebInDays": 30,
"SessionLengthMobileInDays": 30,

25
einterfaces/mfa.go Normal file
View file

@ -0,0 +1,25 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package einterfaces
import (
"github.com/mattermost/platform/model"
)
type MfaInterface interface {
GenerateQrCode(team *model.Team, user *model.User) ([]byte, *model.AppError)
Activate(user *model.User, token string) *model.AppError
Deactivate(userId string) *model.AppError
ValidateToken(secret, token string) (bool, *model.AppError)
}
var theMfaInterface MfaInterface
func RegisterMfaInterface(newInterface MfaInterface) {
theMfaInterface = newInterface
}
func GetMfaInterface() MfaInterface {
return theMfaInterface
}

View file

@ -1291,6 +1291,14 @@
"id": "api.templates.welcome_subject",
"translation": "You joined {{ .TeamDisplayName }}"
},
{
"id": "api.user.update_mfa.not_available.app_error",
"translation": "MFA not configured or available on this server"
},
{
"id": "api.user.generate_mfa_qr.not_available.app_error",
"translation": "MFA not configured or available on this server"
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
"translation": "Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v"
@ -1327,6 +1335,14 @@
"id": "api.user.authorize_oauth_user.unsupported.app_error",
"translation": "Unsupported OAuth service provider"
},
{
"id": "api.user.check_user_mfa.not_available.app_error",
"translation": "MFA is not configured or supported on this server"
},
{
"id": "api.user.check_user_mfa.bad_code.app_error",
"translation": "Invalid MFA token."
},
{
"id": "api.user.check_user_login_attempts.too_many.app_error",
"translation": "Your account is locked because of too many failed password attempts. Please reset your password."
@ -1739,6 +1755,42 @@
"id": "ent.compliance.run_started.info",
"translation": "Compliance export started for job '{{.JobName}}' at '{{.FilePath}}'"
},
{
"id": "ent.mfa.license_disable.app_error",
"translation": "Your license does not support using multi-factor authentication"
},
{
"id": "ent.mfa.generate_qr_code.create_code.app_error",
"translation": "Error generating QR code"
},
{
"id": "ent.mfa.generate_qr_code.save_secret.app_error",
"translation": "Error saving the MFA secret"
},
{
"id": "ent.mfa.activate.authenticate.app_error",
"translation": "Error attempting to authenticate MFA token"
},
{
"id": "ent.mfa.activate.bad_token.app_error",
"translation": "Invalid MFA token"
},
{
"id": "ent.mfa.activate.save_active.app_erro",
"translation": "Unable to update MFA active status for the user"
},
{
"id": "ent.mfa.deactivate.save_active.app_erro",
"translation": "Unable to update MFA active status for the user"
},
{
"id": "ent.mfa.deactivate.save_secret.app_error",
"translation": "Error clearing the MFA secret"
},
{
"id": "ent.mfa.validate_token.authenticate.app_error",
"translation": "Error trying to authenticate MFA token"
},
{
"id": "ent.ldap.do_login.bind_admin_user.app_error",
"translation": "Unable to bind to LDAP server. Check BindUsername and BindPassword."
@ -3135,6 +3187,14 @@
"id": "store.sql_team.update_display_name.app_error",
"translation": "We couldn't update the team name"
},
{
"id": "store.sql_user.update_mfa_secret.app_error",
"translation": "We encountered an error updating the user's MFA secret"
},
{
"id": "store.sql_user.update_mfa_active.app_error",
"translation": "We encountered an error updating the user's MFA active status"
},
{
"id": "store.sql_user.analytics_unique_user_count.app_error",
"translation": "We couldn't get the unique user count"

View file

@ -29,7 +29,9 @@ import (
_ "github.com/mattermost/platform/model/gitlab"
// Enterprise Deps
_ "github.com/dgryski/dgoogauth"
_ "github.com/go-ldap/ldap"
_ "github.com/mattermost/rsc/qr"
)
//ENTERPRISE_IMPORTS

View file

@ -301,6 +301,42 @@ func (c *Client) Logout() (*Result, *AppError) {
}
}
func (c *Client) CheckMfa(method, teamName, loginId string) (*Result, *AppError) {
m := make(map[string]string)
m["method"] = method
m["team_name"] = teamName
m["login_id"] = loginId
if r, err := c.DoApiPost("/users/mfa", MapToJson(m)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
func (c *Client) GenerateMfaQrCode() (*Result, *AppError) {
if r, err := c.DoApiGet("/users/generate_mfa_qr", "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), r.Body}, nil
}
}
func (c *Client) UpdateMfa(activate bool, token string) (*Result, *AppError) {
m := make(map[string]interface{})
m["activate"] = activate
m["token"] = token
if r, err := c.DoApiPost("/users/update_mfa", StringInterfaceToJson(m)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
func (c *Client) SetOAuthToken(token string) {
c.AuthToken = token
c.AuthType = HEADER_TOKEN

View file

@ -46,6 +46,7 @@ type ServiceSettings struct {
EnableDeveloper *bool
EnableSecurityFixAlert *bool
EnableInsecureOutgoingConnections *bool
EnableMultifactorAuthentication *bool
AllowCorsFrom *string
SessionLengthWebInDays *int
SessionLengthMobileInDays *int
@ -275,6 +276,11 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.EnableInsecureOutgoingConnections = false
}
if o.ServiceSettings.EnableMultifactorAuthentication == nil {
o.ServiceSettings.EnableMultifactorAuthentication = new(bool)
*o.ServiceSettings.EnableMultifactorAuthentication = false
}
if o.TeamSettings.RestrictTeamNames == nil {
o.TeamSettings.RestrictTeamNames = new(bool)
*o.TeamSettings.RestrictTeamNames = true

View file

@ -34,6 +34,7 @@ type Customer struct {
type Features struct {
Users *int `json:"users"`
LDAP *bool `json:"ldap"`
MFA *bool `json:"mfa"`
GoogleSSO *bool `json:"google_sso"`
Compliance *bool `json:"compliance"`
}
@ -49,6 +50,11 @@ func (f *Features) SetDefaults() {
*f.LDAP = true
}
if f.MFA == nil {
f.MFA = new(bool)
*f.MFA = true
}
if f.GoogleSSO == nil {
f.GoogleSSO = new(bool)
*f.GoogleSSO = true

View file

@ -15,17 +15,19 @@ import (
)
const (
ROLE_TEAM_ADMIN = "admin"
ROLE_SYSTEM_ADMIN = "system_admin"
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
USER_OFFLINE = "offline"
USER_AWAY = "away"
USER_ONLINE = "online"
USER_NOTIFY_ALL = "all"
USER_NOTIFY_MENTION = "mention"
USER_NOTIFY_NONE = "none"
DEFAULT_LOCALE = "en"
ROLE_TEAM_ADMIN = "admin"
ROLE_SYSTEM_ADMIN = "system_admin"
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
USER_OFFLINE = "offline"
USER_AWAY = "away"
USER_ONLINE = "online"
USER_NOTIFY_ALL = "all"
USER_NOTIFY_MENTION = "mention"
USER_NOTIFY_NONE = "none"
DEFAULT_LOCALE = "en"
USER_AUTH_SERVICE_EMAIL = "email"
USER_AUTH_SERVICE_USERNAME = "username"
)
type User struct {
@ -54,6 +56,8 @@ type User struct {
LastPictureUpdate int64 `json:"last_picture_update,omitempty"`
FailedAttempts int `json:"failed_attempts,omitempty"`
Locale string `json:"locale"`
MfaActive bool `json:"mfa_active,omitempty"`
MfaSecret string `json:"mfa_secret,omitempty"`
}
// IsValid validates the user and returns an error if it isn't configured
@ -140,6 +144,8 @@ func (u *User) PreSave() {
u.LastPasswordUpdate = u.CreateAt
u.MfaActive = false
if u.Locale == "" {
u.Locale = DEFAULT_LOCALE
}

View file

@ -40,6 +40,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
table.ColMap("NotifyProps").SetMaxSize(2000)
table.ColMap("ThemeProps").SetMaxSize(2000)
table.ColMap("Locale").SetMaxSize(5)
table.ColMap("MfaSecret").SetMaxSize(128)
table.SetUniqueTogether("Email", "TeamId")
table.SetUniqueTogether("Username", "TeamId")
}
@ -50,6 +51,9 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
func (us SqlUserStore) UpgradeSchemaIfNeeded() {
// ADDED for 2.0 REMOVE for 2.4
us.CreateColumnIfNotExists("Users", "Locale", "varchar(5)", "character varying(5)", model.DEFAULT_LOCALE)
// ADDED for 2.2 REMOVE for 2.6
us.CreateColumnIfNotExists("Users", "MfaActive", "tinyint(1)", "boolean", "0")
us.CreateColumnIfNotExists("Users", "MfaSecret", "varchar(128)", "character varying(128)", "")
}
func (us SqlUserStore) CreateIndexesIfNotExists() {
@ -141,6 +145,8 @@ func (us SqlUserStore) Update(user *model.User, allowActiveUpdate bool) StoreCha
user.LastPingAt = oldUser.LastPingAt
user.EmailVerified = oldUser.EmailVerified
user.FailedAttempts = oldUser.FailedAttempts
user.MfaSecret = oldUser.MfaSecret
user.MfaActive = oldUser.MfaActive
if !allowActiveUpdate {
user.Roles = oldUser.Roles
@ -346,6 +352,50 @@ func (us SqlUserStore) UpdateAuthData(userId, service, authData, email string) S
return storeChannel
}
func (us SqlUserStore) UpdateMfaSecret(userId, secret string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
updateAt := model.GetMillis()
if _, err := us.GetMaster().Exec("UPDATE Users SET MfaSecret = :Secret, UpdateAt = :UpdateAt WHERE Id = :UserId", map[string]interface{}{"Secret": secret, "UpdateAt": updateAt, "UserId": userId}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.UpdateMfaSecret", "store.sql_user.update_mfa_secret.app_error", nil, "id="+userId+", "+err.Error())
} else {
result.Data = userId
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) UpdateMfaActive(userId string, active bool) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
updateAt := model.GetMillis()
if _, err := us.GetMaster().Exec("UPDATE Users SET MfaActive = :Active, UpdateAt = :UpdateAt WHERE Id = :UserId", map[string]interface{}{"Active": active, "UpdateAt": updateAt, "UserId": userId}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.UpdateMfaActive", "store.sql_user.update_mfa_active.app_error", nil, "id="+userId+", "+err.Error())
} else {
result.Data = userId
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) Get(id string) StoreChannel {
storeChannel := make(StoreChannel)

View file

@ -502,3 +502,47 @@ func TestUserUnreadCount(t *testing.T) {
t.Fatal("should have 3 unread messages")
}
}
func TestUserStoreUpdateMfaSecret(t *testing.T) {
Setup()
u1 := model.User{}
u1.TeamId = model.NewId()
u1.Email = model.NewId()
Must(store.User().Save(&u1))
time.Sleep(100 * time.Millisecond)
if err := (<-store.User().UpdateMfaSecret(u1.Id, "12345")).Err; err != nil {
t.Fatal(err)
}
// should pass, no update will occur though
if err := (<-store.User().UpdateMfaSecret("junk", "12345")).Err; err != nil {
t.Fatal(err)
}
}
func TestUserStoreUpdateMfaActive(t *testing.T) {
Setup()
u1 := model.User{}
u1.TeamId = model.NewId()
u1.Email = model.NewId()
Must(store.User().Save(&u1))
time.Sleep(100 * time.Millisecond)
if err := (<-store.User().UpdateMfaActive(u1.Id, true)).Err; err != nil {
t.Fatal(err)
}
if err := (<-store.User().UpdateMfaActive(u1.Id, false)).Err; err != nil {
t.Fatal(err)
}
// should pass, no update will occur though
if err := (<-store.User().UpdateMfaActive("junk", true)).Err; err != nil {
t.Fatal(err)
}
}

View file

@ -117,6 +117,8 @@ type UserStore interface {
UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel
UpdatePassword(userId, newPassword string) StoreChannel
UpdateAuthData(userId, service, authData, email string) StoreChannel
UpdateMfaSecret(userId, secret string) StoreChannel
UpdateMfaActive(userId string, active bool) StoreChannel
Get(id string) StoreChannel
GetProfiles(teamId string) StoreChannel
GetByEmail(teamId string, email string) StoreChannel

View file

@ -212,6 +212,7 @@ func getClientConfig(c *model.Config) map[string]string {
props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
props["EnableSignInWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithEmail)
props["EnableSignInWithUsername"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithUsername)
props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication)
props["RequireEmailVerification"] = strconv.FormatBool(c.EmailSettings.RequireEmailVerification)
props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail

View file

@ -114,6 +114,7 @@ func getClientLicense(l *model.License) map[string]string {
if IsLicensed {
props["Users"] = strconv.Itoa(*l.Features.Users)
props["LDAP"] = strconv.FormatBool(*l.Features.LDAP)
props["MFA"] = strconv.FormatBool(*l.Features.MFA)
props["GoogleSSO"] = strconv.FormatBool(*l.Features.GoogleSSO)
props["Compliance"] = strconv.FormatBool(*l.Features.Compliance)
props["IssuedAt"] = strconv.FormatInt(l.IssuedAt, 10)

View file

@ -84,6 +84,7 @@ class ServiceSettings extends React.Component {
config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked;
config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
config.ServiceSettings.EnableInsecureOutgoingConnections = ReactDOM.findDOMNode(this.refs.EnableInsecureOutgoingConnections).checked;
config.ServiceSettings.EnableMultifactorAuthentication = ReactDOM.findDOMNode(this.refs.EnableMultifactorAuthentication).checked;
config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked;
config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked;
@ -173,6 +174,58 @@ class ServiceSettings extends React.Component {
saveClass = 'btn btn-primary';
}
let mfaSetting;
if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') {
mfaSetting = (
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnableMultifactorAuthentication'
>
<FormattedMessage
id='admin.service.mfaTitle'
defaultMessage='Enable Multi-factor Authentication:'
/>
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='EnableMultifactorAuthentication'
value='true'
ref='EnableMultifactorAuthentication'
defaultChecked={this.props.config.ServiceSettings.EnableMultifactorAuthentication}
onChange={this.handleChange}
/>
<FormattedMessage
id='admin.service.true'
defaultMessage='true'
/>
</label>
<label className='radio-inline'>
<input
type='radio'
name='EnableMultifactorAuthentication'
value='false'
defaultChecked={!this.props.config.ServiceSettings.EnableMultifactorAuthentication}
onChange={this.handleChange}
/>
<FormattedMessage
id='admin.service.false'
defaultMessage='false'
/>
</label>
<p className='help-text'>
<FormattedMessage
id='admin.service.mfaDesc'
defaultMessage='When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.'
/>
</p>
</div>
</div>
);
}
return (
<div className='wrapper--fixed'>
@ -773,6 +826,8 @@ class ServiceSettings extends React.Component {
</div>
</div>
{mfaSetting}
<div className='form-group'>
<label
className='control-label col-sm-4'

View file

@ -2,69 +2,40 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
import * as Client from 'utils/client.jsx';
import UserStore from 'stores/user_store.jsx';
import {browserHistory} from 'react-router';
import Constants from 'utils/constants.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
var holders = defineMessages({
badTeam: {
id: 'login_email.badTeam',
defaultMessage: 'Bad team name'
},
emailReq: {
id: 'login_email.emailReq',
defaultMessage: 'An email is required'
},
pwdReq: {
id: 'login_email.pwdReq',
defaultMessage: 'A password is required'
},
email: {
id: 'login_email.email',
defaultMessage: 'Email'
},
pwd: {
id: 'login_email.pwd',
defaultMessage: 'Password'
}
});
import {FormattedMessage} from 'react-intl';
import React from 'react';
class LoginEmail extends React.Component {
export default class LoginEmail extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
serverError: ''
serverError: props.serverError
};
}
componentWillReceiveProps(nextProps) {
this.setState({serverError: nextProps.serverError});
}
handleSubmit(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
var state = {};
const name = this.props.teamName;
if (!name) {
state.serverError = formatMessage(holders.badTeam);
this.setState(state);
return;
}
const email = this.refs.email.value.trim();
if (!email) {
state.serverError = formatMessage(holders.emailReq);
state.serverError = Utils.localizeMessage('login_email.emailReq', 'An email is required');
this.setState(state);
return;
}
const password = this.refs.password.value.trim();
if (!password) {
state.serverError = formatMessage(holders.pwdReq);
state.serverError = Utils.localizeMessage('login_email.pwdReq', 'A password is required');
this.setState(state);
return;
}
@ -72,21 +43,7 @@ class LoginEmail extends React.Component {
state.serverError = '';
this.setState(state);
Client.loginByEmail(name, email, password,
() => {
UserStore.setLastEmail(email);
browserHistory.push('/' + name + '/channels/town-square');
},
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
browserHistory.push('/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email));
return;
}
state.serverError = err.message;
this.valid = false;
this.setState(state);
}
);
this.props.submit(Constants.EMAIL_SERVICE, email, password);
}
render() {
let serverError;
@ -110,7 +67,6 @@ class LoginEmail extends React.Component {
priorEmail = decodeURIComponent(emailParam);
}
const {formatMessage} = this.props.intl;
return (
<form onSubmit={this.handleSubmit}>
<div className='signup__email-container'>
@ -125,7 +81,7 @@ class LoginEmail extends React.Component {
name='email'
defaultValue={priorEmail}
ref='email'
placeholder={formatMessage(holders.email)}
placeholder={Utils.localizeMessage('login_email.email', 'Email')}
spellCheck='false'
/>
</div>
@ -136,7 +92,7 @@ class LoginEmail extends React.Component {
className='form-control'
name='password'
ref='password'
placeholder={formatMessage(holders.pwd)}
placeholder={Utils.localizeMessage('login_email.pwd', 'Password')}
spellCheck='false'
/>
</div>
@ -160,8 +116,6 @@ LoginEmail.defaultProps = {
};
LoginEmail.propTypes = {
intl: intlShape.isRequired,
teamName: React.PropTypes.string.isRequired
submit: React.PropTypes.func.isRequired,
serverError: React.PropTypes.string
};
export default injectIntl(LoginEmail);

View file

@ -2,68 +2,39 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
import * as Client from 'utils/client.jsx';
import Constants from 'utils/constants.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router';
const holders = defineMessages({
badTeam: {
id: 'login_ldap.badTeam',
defaultMessage: 'Bad team name'
},
idReq: {
id: 'login_ldap.idlReq',
defaultMessage: 'An LDAP ID is required'
},
pwdReq: {
id: 'login_ldap.pwdReq',
defaultMessage: 'An LDAP password is required'
},
username: {
id: 'login_ldap.username',
defaultMessage: 'LDAP Username'
},
pwd: {
id: 'login_ldap.pwd',
defaultMessage: 'LDAP Password'
}
});
import {FormattedMessage} from 'react-intl';
import React from 'react';
class LoginLdap extends React.Component {
export default class LoginLdap extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
serverError: ''
serverError: props.serverError
};
}
componentWillReceiveProps(nextProps) {
this.setState({serverError: nextProps.serverError});
}
handleSubmit(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
var state = {};
const teamName = this.props.teamName;
if (!teamName) {
state.serverError = formatMessage(holders.badTeam);
this.setState(state);
return;
}
const state = {};
const id = this.refs.id.value.trim();
if (!id) {
state.serverError = formatMessage(holders.idReq);
state.serverError = Utils.localizeMessage('login_ldap.idlReq', 'An LDAP ID is required');
this.setState(state);
return;
}
const password = this.refs.password.value.trim();
if (!password) {
state.serverError = formatMessage(holders.pwdReq);
state.serverError = Utils.localizeMessage('login_ldap.pwdReq', 'An LDAP password is required');
this.setState(state);
return;
}
@ -71,20 +42,7 @@ class LoginLdap extends React.Component {
state.serverError = '';
this.setState(state);
Client.loginByLdap(teamName, id, password,
() => {
const redirect = Utils.getUrlParameter('redirect');
if (redirect) {
browserHistory.push(decodeURIComponent(redirect));
} else {
browserHistory.push('/' + teamName + '/channels/town-square');
}
},
(err) => {
state.serverError = err.message;
this.setState(state);
}
);
this.props.submit(Constants.LDAP_SERVICE, id, password);
}
render() {
let serverError;
@ -93,7 +51,7 @@ class LoginLdap extends React.Component {
serverError = <label className='control-label'>{this.state.serverError}</label>;
errorClass = ' has-error';
}
const {formatMessage} = this.props.intl;
return (
<form onSubmit={this.handleSubmit}>
<div className='signup__email-container'>
@ -105,7 +63,7 @@ class LoginLdap extends React.Component {
autoFocus={true}
className='form-control'
ref='id'
placeholder={formatMessage(holders.username)}
placeholder={Utils.localizeMessage('login_ldap.username', 'LDAP Username')}
spellCheck='false'
/>
</div>
@ -114,7 +72,7 @@ class LoginLdap extends React.Component {
type='password'
className='form-control'
ref='password'
placeholder={formatMessage(holders.pwd)}
placeholder={Utils.localizeMessage('login_ldap.pwd', 'LDAP Password')}
spellCheck='false'
/>
</div>
@ -138,8 +96,6 @@ LoginLdap.defaultProps = {
};
LoginLdap.propTypes = {
intl: intlShape.isRequired,
teamName: React.PropTypes.string.isRequired
serverError: React.PropTypes.string,
submit: React.PropTypes.func.isRequired
};
export default injectIntl(LoginLdap);

View file

@ -0,0 +1,92 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
import React from 'react';
export default class LoginMfa extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
serverError: ''
};
}
handleSubmit(e) {
e.preventDefault();
const state = {};
const token = this.refs.token.value.trim();
if (!token) {
state.serverError = Utils.localizeMessage('login_mfa.tokenReq', 'Please enter an MFA token');
this.setState(state);
return;
}
state.serverError = '';
this.setState(state);
this.props.submit(this.props.method, this.props.loginId, this.props.password, token);
}
render() {
let serverError;
let errorClass = '';
if (this.state.serverError) {
serverError = <label className='control-label'>{this.state.serverError}</label>;
errorClass = ' has-error';
}
return (
<form onSubmit={this.handleSubmit}>
<div className='signup__email-container'>
<p>
<FormattedMessage
id='login_mfa.enterToken'
defaultMessage="To complete the sign in process, please enter a token from your smartphone's authenticator"
/>
</p>
<div className={'form-group' + errorClass}>
{serverError}
</div>
<div className={'form-group' + errorClass}>
<input
type='text'
className='form-control'
name='token'
ref='token'
placeholder={Utils.localizeMessage('login_mfa.token', 'MFA Token')}
spellCheck='false'
autoComplete='off'
autoFocus={true}
/>
</div>
<div className='form-group'>
<button
type='submit'
className='btn btn-primary'
>
<FormattedMessage
id='login_mfa.submit'
defaultMessage='Submit'
/>
</button>
</div>
</div>
</form>
);
}
}
LoginMfa.defaultProps = {
};
LoginMfa.propTypes = {
method: React.PropTypes.string.isRequired,
loginId: React.PropTypes.string.isRequired,
password: React.PropTypes.string.isRequired,
submit: React.PropTypes.func.isRequired
};

View file

@ -2,42 +2,10 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
import * as Client from 'utils/client.jsx';
import UserStore from 'stores/user_store.jsx';
import Constants from 'utils/constants.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router';
var holders = defineMessages({
badTeam: {
id: 'login_username.badTeam',
defaultMessage: 'Bad team name'
},
usernameReq: {
id: 'login_username.usernameReq',
defaultMessage: 'A username is required'
},
pwdReq: {
id: 'login_username.pwdReq',
defaultMessage: 'A password is required'
},
verifyEmailError: {
id: 'login_username.verifyEmailError',
defaultMessage: 'Please verify your email address. Check your inbox for an email.'
},
userNotFoundError: {
id: 'login_username.userNotFoundError',
defaultMessage: "We couldn't find an existing account matching your username for this team."
},
username: {
id: 'login_username.username',
defaultMessage: 'Username'
},
pwd: {
id: 'login_username.pwd',
defaultMessage: 'Password'
}
});
import {FormattedMessage} from 'react-intl';
import React from 'react';
@ -48,31 +16,26 @@ export default class LoginUsername extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
serverError: ''
serverError: props.serverError
};
}
componentWillReceiveProps(nextProps) {
this.setState({serverError: nextProps.serverError});
}
handleSubmit(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
var state = {};
const name = this.props.teamName;
if (!name) {
state.serverError = formatMessage(holders.badTeam);
this.setState(state);
return;
}
const state = {};
const username = this.refs.username.value.trim();
if (!username) {
state.serverError = formatMessage(holders.usernameReq);
state.serverError = Utils.localizeMessage('login_username.usernameReq', 'A username is required');
this.setState(state);
return;
}
const password = this.refs.password.value.trim();
if (!password) {
state.serverError = formatMessage(holders.pwdReq);
state.serverError = Utils.localizeMessage('login_username.pwdReq', 'A password is required');
this.setState(state);
return;
}
@ -80,30 +43,7 @@ export default class LoginUsername extends React.Component {
state.serverError = '';
this.setState(state);
Client.loginByUsername(name, username, password,
() => {
UserStore.setLastUsername(username);
const redirect = Utils.getUrlParameter('redirect');
if (redirect) {
browserHistory.push(decodeURIComponent(redirect));
} else {
browserHistory.push('/' + name + '/channels/town-square');
}
},
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
state.serverError = formatMessage(holders.verifyEmailError);
} else if (err.id === 'store.sql_user.get_by_username.app_error') {
state.serverError = formatMessage(holders.userNotFoundError);
} else {
state.serverError = err.message;
}
this.valid = false;
this.setState(state);
}
);
this.props.submit(Constants.USERNAME_SERVICE, username, password);
}
render() {
let serverError;
@ -127,7 +67,6 @@ export default class LoginUsername extends React.Component {
priorUsername = decodeURIComponent(emailParam);
}
const {formatMessage} = this.props.intl;
return (
<form onSubmit={this.handleSubmit}>
<div className='signup__email-container'>
@ -142,7 +81,7 @@ export default class LoginUsername extends React.Component {
name='username'
defaultValue={priorUsername}
ref='username'
placeholder={formatMessage(holders.username)}
placeholder={Utils.localizeMessage('login_username.username', 'Username')}
spellCheck='false'
/>
</div>
@ -153,7 +92,7 @@ export default class LoginUsername extends React.Component {
className='form-control'
name='password'
ref='password'
placeholder={formatMessage(holders.pwd)}
placeholder={Utils.localizeMessage('login_username.pwd', 'Password')}
spellCheck='false'
/>
</div>
@ -177,8 +116,6 @@ LoginUsername.defaultProps = {
};
LoginUsername.propTypes = {
intl: intlShape.isRequired,
teamName: React.PropTypes.string.isRequired
serverError: React.PropTypes.string,
submit: React.PropTypes.func.isRequired
};
export default injectIntl(LoginUsername);

View file

@ -1,14 +1,17 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import LoginEmail from './login_email.jsx';
import LoginUsername from './login_username.jsx';
import LoginLdap from './login_ldap.jsx';
import LoginEmail from './components/login_email.jsx';
import LoginUsername from './components/login_username.jsx';
import LoginLdap from './components/login_ldap.jsx';
import LoginMfa from './components/login_mfa.jsx';
import * as Utils from 'utils/utils.jsx';
import * as Client from 'utils/client.jsx';
import Constants from 'utils/constants.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Client from 'utils/client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory, Link} from 'react-router';
@ -21,6 +24,8 @@ export default class Login extends React.Component {
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
this.preSubmit = this.preSubmit.bind(this);
this.submit = this.submit.bind(this);
this.state = this.getStateFromStores();
}
@ -46,54 +51,89 @@ export default class Login extends React.Component {
onTeamChange() {
this.setState(this.getStateFromStores());
}
render() {
const currentTeam = this.state.currentTeam;
if (currentTeam == null || !this.state.doneCheckLogin) {
return <div/>;
preSubmit(method, loginId, password) {
if (global.window.mm_config.EnableMultifactorAuthentication !== 'true') {
this.submit(method, loginId, password, '');
return;
}
const teamDisplayName = currentTeam.display_name;
const teamName = currentTeam.name;
const ldapEnabled = global.window.mm_config.EnableLdap === 'true';
const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true';
Client.checkMfa(method, this.state.currentTeam.name, loginId,
(data) => {
if (data.mfa_required === 'true') {
this.setState({showMfa: true, method, loginId, password});
} else {
this.submit(method, loginId, password, '');
}
},
(err) => {
if (method === Constants.EMAIL_SERVICE) {
this.setState({serverEmailError: err.message});
} else if (method === Constants.USERNAME_SERVICE) {
this.setState({serverUsernameError: err.message});
} else if (method === Constants.LDAP_SERVICE) {
this.setState({serverLdapError: err.message});
}
}
);
}
submit(method, loginId, password, token) {
this.setState({showMfa: false, serverEmailError: null, serverUsernameError: null, serverLdapError: null});
let loginMessage = [];
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
loginMessage.push(
<a
className='btn btn-custom-login gitlab'
key='gitlab'
href={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.gitlab'
defaultMessage='with GitLab'
/>
</span>
</a>
const team = this.state.currentTeam.name;
if (method === Constants.EMAIL_SERVICE) {
Client.loginByEmail(team, loginId, password, token,
() => {
UserStore.setLastEmail(loginId);
browserHistory.push('/' + team + '/channels/town-square');
},
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
browserHistory.push('/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(loginId));
return;
}
this.setState({serverEmailError: err.message});
}
);
} else if (method === Constants.USERNAME_SERVICE) {
Client.loginByUsername(team, loginId, password, token,
() => {
UserStore.setLastUsername(loginId);
const redirect = Utils.getUrlParameter('redirect');
if (redirect) {
browserHistory.push(decodeURIComponent(redirect));
} else {
browserHistory.push('/' + team + '/channels/town-square');
}
},
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
this.setState({serverUsernameError: Utils.localizeMessage('login_username.verifyEmailError', 'Please verify your email address. Check your inbox for an email.')});
} else if (err.id === 'store.sql_user.get_by_username.app_error') {
this.setState({serverUsernameError: Utils.localizeMessage('login_username.userNotFoundError', 'We couldn\'t find an existing account matching your username for this team.')});
} else {
this.setState({serverUsernameError: err.message});
}
}
);
} else if (method === Constants.LDAP_SERVICE) {
Client.loginByLdap(team, loginId, password, token,
() => {
const redirect = Utils.getUrlParameter('redirect');
if (redirect) {
browserHistory.push(decodeURIComponent(redirect));
} else {
browserHistory.push('/' + team + '/channels/town-square');
}
},
(err) => {
this.setState({serverLdapError: err.message});
}
);
}
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
loginMessage.push(
<a
className='btn btn-custom-login google'
key='google'
href={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.google'
defaultMessage='with Google Apps'
/>
</span>
</a>
);
}
}
createLoginOptions(currentTeam) {
const extraParam = Utils.getUrlParameter('extra');
let extraBox = '';
if (extraParam) {
@ -130,44 +170,126 @@ export default class Login extends React.Component {
}
}
let emailSignup;
if (global.window.mm_config.EnableSignInWithEmail === 'true') {
emailSignup = (
const teamName = currentTeam.name;
const ldapEnabled = global.window.mm_config.EnableLdap === 'true';
const gitlabSigninEnabled = global.window.mm_config.EnableSignUpWithGitLab === 'true';
const googleSigninEnabled = global.window.mm_config.EnableSignUpWithGoogle === 'true';
const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true';
const emailSigninEnabled = global.window.mm_config.EnableSignInWithEmail === 'true';
const oauthLogins = [];
if (gitlabSigninEnabled) {
oauthLogins.push(
<Link
className='btn btn-custom-login gitlab'
key='gitlab'
to={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.gitlab'
defaultMessage='with GitLab'
/>
</span>
</Link>
);
}
if (googleSigninEnabled) {
oauthLogins.push(
<Link
className='btn btn-custom-login google'
key='google'
to={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.google'
defaultMessage='with Google Apps'
/>
</span>
</Link>
);
}
let emailLogin;
if (emailSigninEnabled) {
emailLogin = (
<LoginEmail
teamName={teamName}
serverError={this.state.serverEmailError}
submit={this.preSubmit}
/>
);
}
if (loginMessage.length > 0 && emailSignup) {
loginMessage = (
<div>
{loginMessage}
<div className='or__container'>
<FormattedMessage
id='login.or'
defaultMessage='or'
/>
if (oauthLogins.length > 0) {
emailLogin = (
<div>
<div className='or__container'>
<FormattedMessage
id='login.or'
defaultMessage='or'
/>
</div>
{emailLogin}
</div>
</div>
);
);
}
}
let forgotPassword;
if (emailSignup) {
forgotPassword = (
<div className='form-group'>
<Link to={'/' + teamName + '/reset_password'}>
<FormattedMessage
id='login.forgot'
defaultMessage='I forgot my password'
/>
</Link>
</div>
let usernameLogin;
if (usernameSigninEnabled) {
usernameLogin = (
<LoginUsername
teamName={teamName}
serverError={this.state.serverUsernameError}
submit={this.preSubmit}
/>
);
if (emailSigninEnabled || oauthLogins.length > 0) {
usernameLogin = (
<div>
<div className='or__container'>
<FormattedMessage
id='login.or'
defaultMessage='or'
/>
</div>
{usernameLogin}
</div>
);
}
}
let userSignUp = null;
let ldapLogin;
if (ldapEnabled) {
ldapLogin = (
<LoginLdap
teamName={teamName}
serverError={this.state.serverLdapError}
submit={this.preSubmit}
/>
);
if (emailSigninEnabled || usernameSigninEnabled || oauthLogins.length > 0) {
ldapLogin = (
<div>
<div className='or__container'>
<FormattedMessage
id='login.or'
defaultMessage='or'
/>
</div>
{ldapLogin}
</div>
);
}
}
let userSignUp;
if (currentTeam.allow_open_invite) {
userSignUp = (
<div>
@ -190,7 +312,21 @@ export default class Login extends React.Component {
);
}
let teamSignUp = null;
let forgotPassword;
if (usernameSigninEnabled || emailSigninEnabled) {
forgotPassword = (
<div className='form-group'>
<Link to={'/' + teamName + '/reset_password'}>
<FormattedMessage
id='login.forgot'
defaultMessage='I forgot my password'
/>
</Link>
</div>
);
}
let teamSignUp;
if (global.window.mm_config.EnableTeamCreation === 'true' && !Utils.isMobileApp()) {
teamSignUp = (
<div className='margin--extra'>
@ -207,54 +343,37 @@ export default class Login extends React.Component {
);
}
let ldapLogin = null;
if (global.window.mm_config.EnableLdap === 'true') {
ldapLogin = (
<LoginLdap
teamName={teamName}
return (
<div>
{extraBox}
{oauthLogins}
{emailLogin}
{usernameLogin}
{ldapLogin}
{userSignUp}
{forgotPassword}
{teamSignUp}
</div>
);
}
render() {
const currentTeam = this.state.currentTeam;
if (currentTeam == null || !this.state.doneCheckLogin) {
return <div/>;
}
let content;
if (this.state.showMfa) {
content = (
<LoginMfa
method={this.state.method}
loginId={this.state.loginId}
password={this.state.password}
submit={this.submit}
/>
);
}
if (ldapEnabled && (loginMessage.length > 0 || emailSignup || usernameSigninEnabled)) {
ldapLogin = (
<div>
<div className='or__container'>
<FormattedMessage
id='login.or'
defaultMessage='or'
/>
</div>
<LoginLdap
teamName={teamName}
/>
</div>
);
}
let usernameLogin = null;
if (global.window.mm_config.EnableSignInWithUsername === 'true') {
usernameLogin = (
<LoginUsername
teamName={teamName}
/>
);
}
if (usernameSigninEnabled && (loginMessage.length > 0 || emailSignup || ldapEnabled)) {
usernameLogin = (
<div>
<div className='or__container'>
<FormattedMessage
id='login.or'
defaultMessage='or'
/>
</div>
<LoginUsername
teamName={teamName}
/>
</div>
);
} else {
content = this.createLoginOptions(currentTeam);
}
return (
@ -275,7 +394,7 @@ export default class Login extends React.Component {
defaultMessage='Sign in to:'
/>
</h5>
<h2 className='signup-team__name'>{teamDisplayName}</h2>
<h2 className='signup-team__name'>{currentTeam.display_name}</h2>
<h2 className='signup-team__subdomain'>
<FormattedMessage
id='login.on'
@ -285,14 +404,7 @@ export default class Login extends React.Component {
}}
/>
</h2>
{extraBox}
{loginMessage}
{emailSignup}
{usernameLogin}
{ldapLogin}
{userSignUp}
{forgotPassword}
{teamSignUp}
{content}
</div>
</div>
</div>

View file

@ -2,7 +2,7 @@
// See License.txt for license information.
import LoadingScreen from 'components/loading_screen.jsx';
import LoginLdap from 'components/login_ldap.jsx';
import LoginLdap from 'components/login/components/login_ldap.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import UserStore from 'stores/user_store.jsx';

View file

@ -47,12 +47,16 @@ class SecurityTab extends React.Component {
super(props);
this.submitPassword = this.submitPassword.bind(this);
this.activateMfa = this.activateMfa.bind(this);
this.deactivateMfa = this.deactivateMfa.bind(this);
this.updateCurrentPassword = this.updateCurrentPassword.bind(this);
this.updateNewPassword = this.updateNewPassword.bind(this);
this.updateConfirmPassword = this.updateConfirmPassword.bind(this);
this.updateMfaToken = this.updateMfaToken.bind(this);
this.getDefaultState = this.getDefaultState.bind(this);
this.createPasswordSection = this.createPasswordSection.bind(this);
this.createSignInSection = this.createSignInSection.bind(this);
this.showQrCode = this.showQrCode.bind(this);
this.state = this.getDefaultState();
}
@ -61,7 +65,9 @@ class SecurityTab extends React.Component {
currentPassword: '',
newPassword: '',
confirmPassword: '',
authService: this.props.user.auth_service
authService: this.props.user.auth_service,
mfaShowQr: false,
mfaToken: ''
};
}
submitPassword(e) {
@ -112,6 +118,51 @@ class SecurityTab extends React.Component {
}
);
}
activateMfa() {
const data = {};
data.activate = true;
data.token = this.state.mfaToken;
Client.updateMfa(data,
() => {
this.props.updateSection('');
AsyncClient.getMe();
this.setState(this.getDefaultState());
},
(err) => {
const state = this.getDefaultState();
if (err.message) {
state.serverError = err.message;
} else {
state.serverError = err;
}
state.mfaError = '';
this.setState(state);
}
);
}
deactivateMfa() {
const data = {};
data.activate = false;
Client.updateMfa(data,
() => {
this.props.updateSection('');
AsyncClient.getMe();
this.setState(this.getDefaultState());
},
(err) => {
const state = this.getDefaultState();
if (err.message) {
state.serverError = err.message;
} else {
state.serverError = err;
}
state.mfaError = '';
this.setState(state);
}
);
}
updateCurrentPassword(e) {
this.setState({currentPassword: e.target.value});
}
@ -121,6 +172,163 @@ class SecurityTab extends React.Component {
updateConfirmPassword(e) {
this.setState({confirmPassword: e.target.value});
}
updateMfaToken(e) {
this.setState({mfaToken: e.target.value});
}
showQrCode(e) {
e.preventDefault();
this.setState({mfaShowQr: true});
}
createMfaSection() {
let updateSectionStatus;
let submit;
if (this.props.activeSection === 'mfa') {
let content;
let extraInfo;
if (this.props.user.mfa_active) {
content = (
<div key='mfaQrCode'>
<a
className='btn btn-primary'
href='#'
onClick={this.deactivateMfa}
>
<FormattedMessage
id='user.settings.mfa.remove'
defaultMessage='Remove MFA from your account'
/>
</a>
<br/>
</div>
);
extraInfo = (
<span>
<FormattedMessage
id='user.settings.mfa.removeHelp'
defaultMessage='Removing multi-factor authentication will make your account more vulnerable to attacks.'
/>
</span>
);
} else if (this.state.mfaShowQr) {
content = (
<div key='mfaButton'>
<label className='col-sm-5 control-label'>
<FormattedMessage
id='user.settings.mfa.qrCode'
defaultMessage='QR Code'
/>
</label>
<div className='col-sm-7'>
<img
className='qr-code-img'
src={'/api/v1/users/generate_mfa_qr?time=' + this.props.user.update_at}
/>
</div>
<br/>
<label className='col-sm-5 control-label'>
<FormattedMessage
id='user.settings.mfa.enterToken'
defaultMessage='Token'
/>
</label>
<div className='col-sm-7'>
<input
className='form-control'
type='text'
onChange={this.updateMfaToken}
value={this.state.mfaToken}
/>
</div>
</div>
);
extraInfo = (
<span>
<FormattedMessage
id='user.settings.mfa.addHelp'
defaultMessage='Please scan the QR code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app.'
/>
</span>
);
submit = this.activateMfa;
} else {
content = (
<div key='mfaQrCode'>
<a
className='btn btn-primary'
href='#'
onClick={this.showQrCode}
>
<FormattedMessage
id='user.settings.mfa.add'
defaultMessage='Add MFA to your account'
/>
</a>
<br/>
</div>
);
extraInfo = (
<span>
<FormattedMessage
id='user.settings.mfa.addHelp'
defaultMessage='To add multi-factor authentication to your account you must have a smartphone with Google Authenticator installed.'
/>
</span>
);
}
const inputs = [];
inputs.push(
<div
key='mfaSetting'
className='form-group'
>
{content}
</div>
);
updateSectionStatus = function resetSection(e) {
this.props.updateSection('');
this.setState({mfaToken: '', mfaShowQr: false, mfaError: null});
e.preventDefault();
}.bind(this);
return (
<SettingItemMax
title={Utils.localizeMessage('user.settings.mfa.title', 'Multi-factor Authentication')}
inputs={inputs}
extraInfo={extraInfo}
submit={submit}
server_error={this.state.serverError}
client_error={this.state.mfaError}
updateSection={updateSectionStatus}
/>
);
}
let describe;
if (this.props.user.mfa_active) {
describe = Utils.localizeMessage('user.settings.security.active', 'Active');
} else {
describe = Utils.localizeMessage('user.settings.security.inactive', 'Inactive');
}
updateSectionStatus = function updateSection() {
this.props.updateSection('mfa');
}.bind(this);
return (
<SettingItemMin
title={Utils.localizeMessage('user.settings.mfa.title', 'Multi-factor Authentication')}
describe={describe}
updateSection={updateSectionStatus}
/>
);
}
createPasswordSection() {
let updateSectionStatus;
@ -316,7 +524,6 @@ class SecurityTab extends React.Component {
const user = this.props.user;
if (this.props.activeSection === 'signin') {
const inputs = [];
const teamName = TeamStore.getCurrent().name;
let emailOption;
@ -398,6 +605,7 @@ class SecurityTab extends React.Component {
);
}
const inputs = [];
inputs.push(
<div key='userSignInOption'>
{emailOption}
@ -463,17 +671,22 @@ class SecurityTab extends React.Component {
}
render() {
const passwordSection = this.createPasswordSection();
let signInSection;
let numMethods = 0;
numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods;
numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods;
numMethods = global.window.mm_config.EnableLdap === 'true' ? numMethods + 1 : numMethods;
if (global.window.mm_config.EnableSignUpWithEmail && numMethods > 0) {
let signInSection;
if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) {
signInSection = this.createSignInSection();
}
let mfaSection;
if (global.window.mm_config.EnableMultifactorAuthentication === 'true' && global.window.mm_license.IsLicensed === 'true') {
mfaSection = this.createMfaSection();
}
return (
<div>
<div className='modal-header'>
@ -512,6 +725,8 @@ class SecurityTab extends React.Component {
<div className='divider-dark first'/>
{passwordSection}
<div className='divider-light'/>
{mfaSection}
<div className='divider-light'/>
{signInSection}
<div className='divider-dark'/>
<br></br>

View file

@ -328,6 +328,8 @@
"admin.select_team.close": "Close",
"admin.select_team.select": "Select",
"admin.select_team.selectTeam": "Select Team",
"admin.service.mfaTitle": "Enable Multi-factor Authentication:",
"admin.service.mfaDesc": "When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.",
"admin.service.attemptDescription": "Login attempts allowed before user is locked out and required to reset password via email.",
"admin.service.attemptExample": "Ex \"10\"",
"admin.service.attemptTitle": "Maximum Login Attempts:",
@ -857,6 +859,10 @@
"login.session_expired": " Your session has expired. Please login again.",
"login.signTo": "Sign in to:",
"login.verified": " Email Verified",
"login_mfa.token": "MFA Token",
"login_mfa.enterToken": "To complete the sign in process, please enter a token from your smartphone's authenticator",
"login_mfa.submit": "Submit",
"login_mfa.tokenReq": "Please enter an MFA token",
"login_email.badTeam": "Bad team name",
"login_email.email": "Email",
"login_email.emailReq": "An email is required",

View file

@ -12,7 +12,6 @@ import React from 'react';
import ReactDOM from 'react-dom';
import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router';
import Root from 'components/root.jsx';
import Login from 'components/login.jsx';
import LoggedIn from 'components/logged_in.jsx';
import NotLoggedIn from 'components/not_logged_in.jsx';
import NeedsTeam from 'components/needs_team.jsx';
@ -58,6 +57,8 @@ import OAuthToEmail from 'components/claim/components/oauth_to_email.jsx';
import LDAPToEmail from 'components/claim/components/ldap_to_email.jsx';
import EmailToLDAP from 'components/claim/components/email_to_ldap.jsx';
import Login from 'components/login/login.jsx';
import * as I18n from 'i18n/i18n.jsx';
// This is for anything that needs to be done for ALL react components.

View file

@ -337,13 +337,28 @@ export function logout(success, error) {
});
}
export function loginByEmail(name, email, password, success, error) {
export function checkMfa(method, team, loginId, success, error) {
$.ajax({
url: '/api/v1/users/mfa',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({method, team_name: team, login_id: loginId}),
success,
error: function onError(xhr, status, err) {
var e = handleError('checkMfa', xhr, status, err);
error(e);
}
});
}
export function loginByEmail(name, email, password, token, success, error) {
$.ajax({
url: '/api/v1/users/login',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({name, email, password}),
data: JSON.stringify({name, email, password, token}),
success: function onSuccess(data, textStatus, xhr) {
track('api', 'api_users_login_success', data.team_id, 'email', data.email);
sessionStorage.removeItem(data.id + '_last_error');
@ -381,13 +396,13 @@ export function loginByUsername(name, username, password, success, error) {
});
}
export function loginByLdap(teamName, id, password, success, error) {
export function loginByLdap(teamName, id, password, token, success, error) {
$.ajax({
url: '/api/v1/users/login_ldap',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({teamName, id, password}),
data: JSON.stringify({teamName, id, password, token}),
success: function onSuccess(data, textStatus, xhr) {
track('api', 'api_users_loginLdap_success', data.team_id, 'id', id);
sessionStorage.removeItem(data.id + '_last_error');
@ -1712,3 +1727,18 @@ export function resendVerification(success, error, teamName, email) {
}
});
}
export function updateMfa(data, success, error) {
$.ajax({
url: '/api/v1/users/update_mfa',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success,
error: (xhr, status, err) => {
var e = handleError('updateMfa', xhr, status, err);
error(e);
}
});
}

View file

@ -194,8 +194,9 @@ export default {
OFFTOPIC_CHANNEL: 'off-topic',
GITLAB_SERVICE: 'gitlab',
GOOGLE_SERVICE: 'google',
LDAP_SERVICE: 'ldap',
EMAIL_SERVICE: 'email',
LDAP_SERVICE: 'ldap',
USERNAME_SERVICE: 'username',
SIGNIN_CHANGE: 'signin_change',
SIGNIN_VERIFIED: 'verified',
SESSION_EXPIRED: 'expired',