mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
Add MFA functionality
This commit is contained in:
parent
2aa0d9b8fc
commit
f9a3a4b394
49 changed files with 5845 additions and 361 deletions
12
Godeps/Godeps.json
generated
12
Godeps/Godeps.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
1
Godeps/_workspace/src/github.com/dgryski/dgoogauth/.travis.yml
generated
vendored
Normal file
1
Godeps/_workspace/src/github.com/dgryski/dgoogauth/.travis.yml
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
language: go
|
||||
15
Godeps/_workspace/src/github.com/dgryski/dgoogauth/README.md
generated
vendored
Normal file
15
Godeps/_workspace/src/github.com/dgryski/dgoogauth/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
This is a Go implementation of the Google Authenticator library.
|
||||
|
||||
[](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
|
||||
199
Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth.go
generated
vendored
Normal file
199
Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth.go
generated
vendored
Normal 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()
|
||||
}
|
||||
251
Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth_test.go
generated
vendored
Normal file
251
Godeps/_workspace/src/github.com/dgryski/dgoogauth/googauth_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Godeps/_workspace/src/github.com/mattermost/rsc/gf256/Makefile
generated
vendored
Normal file
8
Godeps/_workspace/src/github.com/mattermost/rsc/gf256/Makefile
generated
vendored
Normal 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
|
||||
85
Godeps/_workspace/src/github.com/mattermost/rsc/gf256/blog_test.go
generated
vendored
Normal file
85
Godeps/_workspace/src/github.com/mattermost/rsc/gf256/blog_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
241
Godeps/_workspace/src/github.com/mattermost/rsc/gf256/gf256.go
generated
vendored
Normal file
241
Godeps/_workspace/src/github.com/mattermost/rsc/gf256/gf256.go
generated
vendored
Normal 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
|
||||
}
|
||||
194
Godeps/_workspace/src/github.com/mattermost/rsc/gf256/gf256_test.go
generated
vendored
Normal file
194
Godeps/_workspace/src/github.com/mattermost/rsc/gf256/gf256_test.go
generated
vendored
Normal 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
|
||||
}
|
||||
4
Godeps/_workspace/src/github.com/mattermost/rsc/qr/Makefile
generated
vendored
Normal file
4
Godeps/_workspace/src/github.com/mattermost/rsc/qr/Makefile
generated
vendored
Normal 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
|
||||
7
Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/Makefile
generated
vendored
Normal file
7
Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/Makefile
generated
vendored
Normal 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
|
||||
149
Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/gen.go
generated
vendored
Normal file
149
Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/gen.go
generated
vendored
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
815
Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/qr.go
generated
vendored
Normal file
815
Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/qr.go
generated
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
133
Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/qr_test.go
generated
vendored
Normal file
133
Godeps/_workspace/src/github.com/mattermost/rsc/qr/coding/qr_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
4
Godeps/_workspace/src/github.com/mattermost/rsc/qr/libqrencode/Makefile
generated
vendored
Normal file
4
Godeps/_workspace/src/github.com/mattermost/rsc/qr/libqrencode/Makefile
generated
vendored
Normal 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
|
||||
149
Godeps/_workspace/src/github.com/mattermost/rsc/qr/libqrencode/qrencode.go
generated
vendored
Normal file
149
Godeps/_workspace/src/github.com/mattermost/rsc/qr/libqrencode/qrencode.go
generated
vendored
Normal 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
|
||||
}
|
||||
400
Godeps/_workspace/src/github.com/mattermost/rsc/qr/png.go
generated
vendored
Normal file
400
Godeps/_workspace/src/github.com/mattermost/rsc/qr/png.go
generated
vendored
Normal 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) }
|
||||
73
Godeps/_workspace/src/github.com/mattermost/rsc/qr/png_test.go
generated
vendored
Normal file
73
Godeps/_workspace/src/github.com/mattermost/rsc/qr/png_test.go
generated
vendored
Normal 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()))
|
||||
}
|
||||
116
Godeps/_workspace/src/github.com/mattermost/rsc/qr/qr.go
generated
vendored
Normal file
116
Godeps/_workspace/src/github.com/mattermost/rsc/qr/qr.go
generated
vendored
Normal 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
|
||||
}
|
||||
506
Godeps/_workspace/src/github.com/mattermost/rsc/qr/web/pic.go
generated
vendored
Normal file
506
Godeps/_workspace/src/github.com/mattermost/rsc/qr/web/pic.go
generated
vendored
Normal 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())
|
||||
}
|
||||
|
||||
1118
Godeps/_workspace/src/github.com/mattermost/rsc/qr/web/play.go
generated
vendored
Normal file
1118
Godeps/_workspace/src/github.com/mattermost/rsc/qr/web/play.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
152
Godeps/_workspace/src/github.com/mattermost/rsc/qr/web/resize/resize.go
generated
vendored
Normal file
152
Godeps/_workspace/src/github.com/mattermost/rsc/qr/web/resize/resize.go
generated
vendored
Normal 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
|
||||
}
|
||||
197
api/user.go
197
api/user.go
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"EnableDeveloper": false,
|
||||
"EnableSecurityFixAlert": true,
|
||||
"EnableInsecureOutgoingConnections": false,
|
||||
"EnableMultifactorAuthentication": false,
|
||||
"AllowCorsFrom": "",
|
||||
"SessionLengthWebInDays": 30,
|
||||
"SessionLengthMobileInDays": 30,
|
||||
|
|
|
|||
25
einterfaces/mfa.go
Normal file
25
einterfaces/mfa.go
Normal 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
|
||||
}
|
||||
60
i18n/en.json
60
i18n/en.json
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
92
webapp/components/login/components/login_mfa.jsx
Normal file
92
webapp/components/login/components/login_mfa.jsx
Normal 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
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue