2018-02-20 17:20:07 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2019-03-04 08:00:22 +00:00
|
|
|
"bytes"
|
2018-02-20 17:20:07 +00:00
|
|
|
"crypto/hmac"
|
2018-02-21 00:51:06 +00:00
|
|
|
"crypto/sha512"
|
2018-02-20 17:20:07 +00:00
|
|
|
"encoding/hex"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/julienschmidt/httprouter"
|
2020-03-27 13:57:14 +00:00
|
|
|
|
2021-10-31 15:43:43 +00:00
|
|
|
"git.nemunai.re/srs/adlin/libadlin"
|
2018-02-20 17:20:07 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const IPgwDMZ = "172.23.200.1"
|
|
|
|
|
|
|
|
type Challenge struct {
|
2020-03-27 13:57:14 +00:00
|
|
|
Accessible []func(*adlin.Student, *http.Request) error
|
|
|
|
Check func(*adlin.Student, *givenToken, int) error
|
2018-02-20 17:20:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Restrictions */
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func noAccessRestriction(*adlin.Student, *http.Request) error {
|
2018-02-20 17:20:07 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func noAccess(*adlin.Student, *http.Request) error {
|
2020-02-27 14:30:07 +00:00
|
|
|
return errors.New("This challenge cannot be accessed this way. ")
|
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func accessFrom(ip string) func(_ *adlin.Student, r *http.Request) error {
|
|
|
|
return func(_ *adlin.Student, r *http.Request) error {
|
2018-02-20 17:20:07 +00:00
|
|
|
if r.Header.Get("X-Forwarded-By") != ip {
|
|
|
|
return errors.New("This challenge is not accessible this way.")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func notAccessFrom(ip string) func(_ *adlin.Student, r *http.Request) error {
|
|
|
|
return func(_ *adlin.Student, r *http.Request) error {
|
2018-02-20 17:20:07 +00:00
|
|
|
if r.Header.Get("X-Forwarded-By") == ip {
|
|
|
|
return errors.New("This challenge is not accessible this way.")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func maxProxy(nb int) func(_ *adlin.Student, r *http.Request) error {
|
|
|
|
return func(_ *adlin.Student, r *http.Request) error {
|
2018-02-20 17:20:07 +00:00
|
|
|
if len(strings.Split(r.Header.Get("X-Forwarded-For"), ",")) > nb {
|
|
|
|
return errors.New("This challenge is not accessible this way.")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func sslOnly(_ *adlin.Student, r *http.Request) error {
|
2018-02-20 17:20:07 +00:00
|
|
|
if r.Header.Get("X-Forwarded-Proto") != "https" {
|
|
|
|
return errors.New("This challenge should be performed over TLS.")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Challenges */
|
|
|
|
|
2021-02-18 00:14:10 +00:00
|
|
|
func challengeOk(s *adlin.Student, t *givenToken, chid int) error {
|
|
|
|
pkey := s.GetPKey()
|
|
|
|
if expectedToken, err := GenerateToken(pkey, 0, []byte(t.Data[0])); err != nil {
|
|
|
|
return err
|
|
|
|
} else if !hmac.Equal(expectedToken, t.token) {
|
|
|
|
return errors.New("This is not the expected token.")
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func challenge42(s *adlin.Student, t *givenToken, chid int) error {
|
2018-02-20 17:20:07 +00:00
|
|
|
pkey := s.GetPKey()
|
|
|
|
if expectedToken, err := GenerateToken(pkey, chid, []byte("42")); err != nil {
|
|
|
|
return err
|
2020-03-27 13:57:14 +00:00
|
|
|
} else if !hmac.Equal(expectedToken, t.token) {
|
2018-02-20 17:20:07 +00:00
|
|
|
return errors.New("This is not the expected token.")
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func challengeDNS(s *adlin.Student, t *givenToken, chid int) error {
|
2018-02-20 17:20:07 +00:00
|
|
|
pkey := s.GetPKey()
|
|
|
|
if expectedToken, err := GenerateToken(pkey, chid, []byte("8dde678132d6c558fc6adaeb9f1d53bf6ec7b876308cf98c48604caa9138523c1ce58b672c87c7e7d9b7248b81804d3940dbf20bf263eeb683244f7c1143712d")); err != nil {
|
|
|
|
return err
|
2020-03-27 13:57:14 +00:00
|
|
|
} else if !hmac.Equal(expectedToken, t.token) {
|
2018-02-20 17:20:07 +00:00
|
|
|
return errors.New("This is not the expected token.")
|
|
|
|
}
|
2020-03-01 17:09:20 +00:00
|
|
|
|
|
|
|
return nil
|
2018-02-20 17:20:07 +00:00
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func challengeTime(s *adlin.Student, t *givenToken, chid int) error {
|
2018-02-20 17:20:07 +00:00
|
|
|
pkey := s.GetPKey()
|
|
|
|
if expectedToken, err := GenerateToken(pkey, chid, []byte(t.Data[0])); err != nil {
|
|
|
|
return err
|
2020-03-27 13:57:14 +00:00
|
|
|
} else if !hmac.Equal(expectedToken, t.token) {
|
2018-02-20 17:20:07 +00:00
|
|
|
return errors.New("This is not the expected token.")
|
|
|
|
} else if t, err := strconv.ParseInt(t.Data[0], 10, 64); err != nil {
|
|
|
|
return err
|
|
|
|
} else {
|
|
|
|
var rt time.Time
|
|
|
|
if t > 3000000000 {
|
2020-03-27 13:57:14 +00:00
|
|
|
rt = time.Unix(t/1000000000, t%1000000000)
|
2018-02-20 17:20:07 +00:00
|
|
|
} else {
|
|
|
|
rt = time.Unix(t, 0)
|
|
|
|
}
|
|
|
|
if rt.After(time.Now()) {
|
|
|
|
return errors.New("You seem to live in the future...")
|
|
|
|
} else if d := rt.Sub(time.Now()); d.Minutes() < -10 {
|
|
|
|
return errors.New(fmt.Sprintf("Your time is %.0f minutes from us", d.Minutes()))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func challengePing(s *adlin.Student, t *givenToken, chid int) error {
|
2019-02-27 04:49:08 +00:00
|
|
|
var expected []byte
|
|
|
|
switch s.Id % 5 {
|
|
|
|
case 1:
|
|
|
|
expected = []byte("baaaaaad")
|
|
|
|
case 2:
|
|
|
|
expected = []byte("baadfood")
|
|
|
|
case 3:
|
|
|
|
expected = []byte("baddcafe")
|
|
|
|
case 4:
|
|
|
|
expected = []byte("cafebabe")
|
|
|
|
default:
|
|
|
|
expected = []byte("deadbeef")
|
|
|
|
}
|
|
|
|
|
2019-03-04 08:00:22 +00:00
|
|
|
// Allow 1 iteration or the 10 ones
|
|
|
|
var expected10 []byte
|
|
|
|
for i := 0; i < 10; i++ {
|
|
|
|
expected10 = append(expected10, expected...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make it case insensitive
|
|
|
|
expectedC := bytes.ToUpper(expected)
|
|
|
|
expected10C := bytes.ToUpper(expected10)
|
|
|
|
|
2019-02-27 04:49:08 +00:00
|
|
|
pkey := s.GetPKey()
|
2019-03-04 08:00:22 +00:00
|
|
|
if expectedToken, err := GenerateToken(pkey, 0, expected); err != nil {
|
2019-02-27 04:49:08 +00:00
|
|
|
return err
|
2019-03-04 08:00:22 +00:00
|
|
|
} else if hmac.Equal(expectedToken, t.token) {
|
|
|
|
return nil
|
|
|
|
} else if expectedToken, err := GenerateToken(pkey, 0, expectedC); err != nil {
|
|
|
|
return err
|
|
|
|
} else if hmac.Equal(expectedToken, t.token) {
|
|
|
|
return nil
|
|
|
|
} else if expectedToken, err := GenerateToken(pkey, 0, expected10); err != nil {
|
|
|
|
return err
|
|
|
|
} else if hmac.Equal(expectedToken, t.token) {
|
2019-02-27 04:49:08 +00:00
|
|
|
return nil
|
2019-03-04 08:00:22 +00:00
|
|
|
} else if expectedToken, err := GenerateToken(pkey, 0, expected10C); err != nil {
|
|
|
|
return err
|
|
|
|
} else if hmac.Equal(expectedToken, t.token) {
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return errors.New("This is not the expected token.")
|
2019-02-27 04:49:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func challengeDisk(s *adlin.Student, t *givenToken, chid int) error {
|
2018-02-22 04:47:38 +00:00
|
|
|
pkey := fmt.Sprintf("%x", s.GetPKey())
|
2018-02-20 17:20:07 +00:00
|
|
|
|
2018-02-22 04:47:38 +00:00
|
|
|
n1, err := strconv.Atoi(t.Data[0][0:2])
|
2018-02-20 17:20:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-02-22 04:47:38 +00:00
|
|
|
n2, err := strconv.Atoi(t.Data[0][2:4])
|
2018-02-20 17:20:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-02-22 04:47:38 +00:00
|
|
|
sum := make([]byte, hex.DecodedLen(len(t.Data[0][4:])))
|
|
|
|
if _, err := hex.Decode(sum, []byte(t.Data[0][4:])); err != nil {
|
2018-02-20 17:20:07 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-02-22 04:47:38 +00:00
|
|
|
if n1+n2 > len(pkey) {
|
2020-03-27 13:57:14 +00:00
|
|
|
n2 = len(pkey) - n1
|
2018-02-22 04:47:38 +00:00
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
expectedToken := sha512.Sum512([]byte(pkey[n1 : n1+n2]))
|
2018-02-20 17:20:07 +00:00
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
if !hmac.Equal(expectedToken[:], sum) {
|
2018-02-20 17:20:07 +00:00
|
|
|
return errors.New("This is not the expected token.")
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
func challengeEMail(s *adlin.Student, t *givenToken, chid int) error {
|
2020-02-28 12:22:31 +00:00
|
|
|
return errors.New("This is not the expected token.")
|
|
|
|
}
|
|
|
|
|
2018-02-20 17:20:07 +00:00
|
|
|
var challenges []Challenge
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
challenges = []Challenge{
|
|
|
|
/* Challenge 1 : 42 */
|
|
|
|
Challenge{
|
2020-03-27 13:57:14 +00:00
|
|
|
Accessible: []func(*adlin.Student, *http.Request) error{noAccessRestriction},
|
|
|
|
Check: challenge42,
|
2018-02-20 17:20:07 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/* Challenge 2 : 42 from DMZ */
|
|
|
|
Challenge{
|
2020-03-27 13:57:14 +00:00
|
|
|
Accessible: []func(*adlin.Student, *http.Request) error{accessFrom(IPgwDMZ)},
|
|
|
|
Check: challenge42,
|
2018-02-20 17:20:07 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/* Challenge 3 : ssl (+ ntp) */
|
|
|
|
Challenge{
|
2020-03-27 13:57:14 +00:00
|
|
|
Accessible: []func(*adlin.Student, *http.Request) error{accessFrom(IPgwDMZ), sslOnly},
|
|
|
|
Check: challengeTime,
|
2018-02-20 17:20:07 +00:00
|
|
|
},
|
|
|
|
|
2019-02-27 04:49:08 +00:00
|
|
|
/* Challenge 4 : DNS TXT */
|
2018-02-20 17:20:07 +00:00
|
|
|
Challenge{
|
2020-03-27 13:57:14 +00:00
|
|
|
Accessible: []func(*adlin.Student, *http.Request) error{accessFrom(IPgwDMZ), sslOnly},
|
|
|
|
Check: challengeDNS,
|
2018-02-20 17:20:07 +00:00
|
|
|
},
|
|
|
|
|
2019-02-27 04:49:08 +00:00
|
|
|
/* Challenge 5 : time net */
|
2018-02-20 17:20:07 +00:00
|
|
|
Challenge{
|
2020-03-27 13:57:14 +00:00
|
|
|
Accessible: []func(*adlin.Student, *http.Request) error{maxProxy(1)},
|
|
|
|
Check: challengeTime,
|
2018-02-20 17:20:07 +00:00
|
|
|
},
|
2019-02-27 04:49:08 +00:00
|
|
|
|
2020-02-27 14:25:01 +00:00
|
|
|
/* Bonus 0 : toctoc (read in source code) */
|
|
|
|
Challenge{
|
2020-03-27 13:57:14 +00:00
|
|
|
Accessible: []func(*adlin.Student, *http.Request) error{noAccessRestriction},
|
|
|
|
Check: challenge42,
|
2020-02-27 14:25:01 +00:00
|
|
|
},
|
|
|
|
|
2019-02-27 04:49:08 +00:00
|
|
|
/* Bonus 1 : echo request */
|
|
|
|
Challenge{
|
2020-03-27 13:57:14 +00:00
|
|
|
Accessible: []func(*adlin.Student, *http.Request) error{noAccessRestriction},
|
|
|
|
Check: challengePing,
|
2019-02-27 04:49:08 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/* Bonus 2 : disk */
|
|
|
|
Challenge{
|
2020-03-27 13:57:14 +00:00
|
|
|
Accessible: []func(*adlin.Student, *http.Request) error{noAccessRestriction},
|
|
|
|
Check: challengeDisk,
|
2019-02-27 04:49:08 +00:00
|
|
|
},
|
|
|
|
|
2020-02-28 12:22:31 +00:00
|
|
|
/* Bonus 3 : mail */
|
|
|
|
Challenge{
|
2020-03-27 13:57:14 +00:00
|
|
|
Accessible: []func(*adlin.Student, *http.Request) error{noAccessRestriction},
|
|
|
|
Check: challengeEMail,
|
2020-02-28 12:22:31 +00:00
|
|
|
},
|
|
|
|
|
2021-02-18 00:14:10 +00:00
|
|
|
/* wg step */
|
|
|
|
Challenge{
|
|
|
|
Accessible: []func(*adlin.Student, *http.Request) error{noAccessRestriction},
|
|
|
|
Check: challengeOk,
|
|
|
|
},
|
|
|
|
|
2019-03-25 21:46:12 +00:00
|
|
|
/* Last : SSH key, see ssh.go:156 in NewKey function */
|
2020-02-27 14:30:07 +00:00
|
|
|
Challenge{
|
2020-03-27 13:57:14 +00:00
|
|
|
Accessible: []func(*adlin.Student, *http.Request) error{noAccess},
|
2020-02-27 14:30:07 +00:00
|
|
|
},
|
2018-02-20 17:20:07 +00:00
|
|
|
}
|
|
|
|
|
2020-02-27 14:28:38 +00:00
|
|
|
router.GET("/challenges", apiHandler(getChallengeList))
|
2020-03-01 17:09:20 +00:00
|
|
|
router.GET("/challenge/:chid", rawHandler(responseHandler(accessibleChallenge)))
|
|
|
|
router.POST("/challenge", rawHandler(responseHandler(challengeHandler(receiveToken))))
|
|
|
|
router.POST("/challenge/:chid", rawHandler(responseHandler(receiveChallenge)))
|
|
|
|
router.POST("/toctoc", rawHandler(responseHandler(definedChallengeHandler(receiveToken, 6))))
|
|
|
|
router.POST("/echorequest", rawHandler(responseHandler(definedChallengeHandler(receiveToken, 7))))
|
|
|
|
router.POST("/testdisk", rawHandler(responseHandler(definedChallengeHandler(receiveToken, 8))))
|
2021-02-18 00:14:10 +00:00
|
|
|
router.POST("/wg-step", rawHandler(responseHandler(definedChallengeHandler(receiveToken, 10))))
|
2018-02-20 17:20:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type givenToken struct {
|
2020-03-27 13:57:14 +00:00
|
|
|
Login string `json:"login"`
|
|
|
|
Challenge int `json:"challenge"`
|
|
|
|
Token string `json:"token"`
|
2018-02-20 17:20:07 +00:00
|
|
|
token []byte
|
|
|
|
Data []string `json:"data"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func getChallengeList(_ httprouter.Params, _ []byte) (interface{}, error) {
|
|
|
|
var ret []int
|
|
|
|
for i := range challenges {
|
|
|
|
ret = append(ret, i)
|
|
|
|
}
|
|
|
|
return ret, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func accessibleChallenge(r *http.Request, ps httprouter.Params, _ []byte) (interface{}, error) {
|
|
|
|
if chid, err := strconv.Atoi(string(ps.ByName("chid"))); err != nil {
|
|
|
|
return nil, err
|
|
|
|
} else if chid == 0 || chid >= len(challenges) {
|
|
|
|
return nil, errors.New("This challenge doesn't exist")
|
|
|
|
} else {
|
2020-03-27 13:57:14 +00:00
|
|
|
for _, a := range challenges[chid-1].Accessible {
|
2018-02-20 17:20:07 +00:00
|
|
|
if err := a(nil, r); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-25 22:14:09 +00:00
|
|
|
func receiveChallenge(r *http.Request, ps httprouter.Params, body []byte) (interface{}, error) {
|
|
|
|
if chid, err := strconv.Atoi(string(ps.ByName("chid"))); err != nil {
|
|
|
|
return nil, err
|
|
|
|
} else if chid == 0 {
|
|
|
|
return nil, errors.New("This challenge doesn't exist")
|
|
|
|
} else if chid <= len(challenges) {
|
|
|
|
return nil, errors.New("This is not the good way to hit this challenge")
|
|
|
|
} else {
|
|
|
|
var gt givenToken
|
|
|
|
if err := json.Unmarshal(body, >); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if gt.Token != PongSecret {
|
|
|
|
return nil, errors.New("This is not the expected token.")
|
|
|
|
}
|
|
|
|
|
2021-03-07 11:39:38 +00:00
|
|
|
var std *adlin.Student
|
2019-03-25 22:14:09 +00:00
|
|
|
|
|
|
|
if stdid, err := strconv.Atoi(gt.Login); err == nil {
|
2020-03-27 13:57:14 +00:00
|
|
|
if std, err = adlin.GetStudent(stdid); err != nil {
|
2019-03-25 22:14:09 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2020-03-27 13:57:14 +00:00
|
|
|
} else if std, err = adlin.GetStudentByLogin(gt.Login); err != nil {
|
2019-03-25 22:14:09 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := std.UnlockNewChallenge(chid, strings.Join(gt.Data, " ")); err != nil {
|
|
|
|
if _, err := std.UpdateUnlockedChallenge(chid, strings.Join(gt.Data, " ")); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-27 04:49:08 +00:00
|
|
|
func receiveToken(r *http.Request, body []byte, chid int) (interface{}, error) {
|
2018-02-20 17:20:07 +00:00
|
|
|
var gt givenToken
|
|
|
|
if err := json.Unmarshal(body, >); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
gt.token = make([]byte, hex.DecodedLen(len(gt.Token)))
|
|
|
|
if _, err := hex.Decode(gt.token, []byte(gt.Token)); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-02-22 00:23:13 +00:00
|
|
|
// Find challenge ID
|
2019-02-27 04:49:08 +00:00
|
|
|
if gt.Challenge >= 1 {
|
|
|
|
chid = gt.Challenge
|
2018-02-22 00:23:13 +00:00
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
if chid <= 0 || chid-1 > len(challenges) {
|
2018-02-20 17:20:07 +00:00
|
|
|
return nil, errors.New("This challenge doesn't exist")
|
2018-02-22 00:23:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Is the challenge accessible?
|
2020-03-27 13:57:14 +00:00
|
|
|
for _, a := range challenges[chid-1].Accessible {
|
2018-02-22 00:23:13 +00:00
|
|
|
if err := a(nil, r); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 13:57:14 +00:00
|
|
|
if std, err := adlin.GetStudentByLogin(gt.Login); err != nil {
|
2018-02-20 17:20:07 +00:00
|
|
|
return nil, err
|
|
|
|
} else {
|
2021-03-07 11:39:38 +00:00
|
|
|
if err := challenges[chid-1].Check(std, >, chid); err != nil {
|
2018-02-20 17:20:07 +00:00
|
|
|
log.Printf("%s just try ch#%d: %s\n", std.Login, chid, err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := std.UnlockNewChallenge(chid, gt.Token); err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("%s just unlock ch#%d\n", std.Login, chid)
|
|
|
|
return "Success", nil
|
|
|
|
}
|
|
|
|
}
|