M karajo.go => karajo.go +2 -0
@@ 50,6 50,7 @@ var (
type Karajo struct {
httpd *libhttp.Server
env *Environment
+ sm *sessionManager
}
// Sign generate hex string of HMAC + SHA256 of payload using the secret.
@@ 75,6 76,7 @@ func New(env *Environment) (k *Karajo, err error) {
k = &Karajo{
env: env,
+ sm: newSessionManager(),
}
mlog.SetPrefix(env.Name + `:`)
M karajo_httpd.go => karajo_httpd.go +84 -0
@@ 14,6 14,7 @@ import (
"strings"
"time"
+ liberrors "github.com/shuLhan/share/lib/errors"
libhttp "github.com/shuLhan/share/lib/http"
)
@@ 22,6 23,8 @@ const HeaderNameXKarajoSign = `X-Karajo-Sign`
// List of HTTP API.
const (
+ apiAuthLogin = `/karajo/api/auth/login`
+
apiEnvironment = `/karajo/api/environment`
apiJobHttp = `/karajo/api/job_http`
@@ 40,6 43,17 @@ const (
paramNameCounter = `counter`
paramNameID = `id`
paramNameKarajoEpoch = `_karajo_epoch`
+ paramNameName = `name`
+ paramNamePassword = `password`
+)
+
+// List of errors related to HTTP APIs.
+var (
+ errAuthLogin = liberrors.E{
+ Code: http.StatusBadRequest,
+ Name: `ERR_AUTH_LOGIN`,
+ Message: `invalid user name and/or password`,
+ }
)
// initHttpd initialize the HTTP server, including registering its endpoints
@@ 82,6 96,17 @@ func (k *Karajo) registerApis() (err error) {
var logp = `registerApis`
err = k.httpd.RegisterEndpoint(&libhttp.Endpoint{
+ Method: libhttp.RequestMethodPost,
+ Path: apiAuthLogin,
+ RequestType: libhttp.RequestTypeForm,
+ ResponseType: libhttp.ResponseTypeJSON,
+ Call: k.apiAuthLogin,
+ })
+ if err != nil {
+ return fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ err = k.httpd.RegisterEndpoint(&libhttp.Endpoint{
Method: libhttp.RequestMethodGet,
Path: apiEnvironment,
RequestType: libhttp.RequestTypeNone,
@@ 192,6 217,65 @@ func (k *Karajo) registerJobsHook() (err error) {
return nil
}
+// apiAuthLogin authenticate user using name and password.
+//
+// A valid user's account will receive authorization cookie named `karajo`
+// that can be used as authorization for subsequent request.
+//
+// Request format,
+//
+// POST /karajo/api/auth/login
+// Content-Type: application/x-www-form-urlencoded
+//
+// name=&password=
+//
+// List of response,
+//
+// - 200 OK: success.
+// - 400 ERR_AUTH_LOGIN: invalid name and/or password.
+// - 500 ERR_INTERNAL: internal server error.
+func (k *Karajo) apiAuthLogin(epr *libhttp.EndpointRequest) (respBody []byte, err error) {
+ var (
+ logp = `apiAuthLogin`
+ name = epr.HttpRequest.Form.Get(paramNameName)
+ pass = epr.HttpRequest.Form.Get(paramNamePassword)
+ )
+
+ name = strings.TrimSpace(name)
+ if len(name) == 0 {
+ return nil, &errAuthLogin
+ }
+
+ pass = strings.TrimSpace(pass)
+ if len(pass) == 0 {
+ return nil, &errAuthLogin
+ }
+
+ var user = k.env.Users[name]
+ if user == nil {
+ return nil, &errAuthLogin
+ }
+
+ if !user.authenticate(pass) {
+ return nil, &errAuthLogin
+ }
+
+ err = k.sessionNew(epr.HttpWriter, user)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ var res = &libhttp.EndpointResponse{}
+
+ res.Code = http.StatusOK
+ respBody, err = json.Marshal(res)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ return respBody, nil
+}
+
func (k *Karajo) apiEnvironment(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
var (
logp = `apiEnvironment`
A karajo_httpd_test.go => karajo_httpd_test.go +118 -0
@@ 0,0 1,118 @@
+// SPDX-FileCopyrightText: 2023 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package karajo
+
+import (
+ "math/rand"
+ "net/http"
+ "net/http/httptest"
+ "net/http/httputil"
+ "net/url"
+ "testing"
+
+ libhttp "github.com/shuLhan/share/lib/http"
+ "github.com/shuLhan/share/lib/test"
+)
+
+func TestKarajo_apiAuthLogin(t *testing.T) {
+ rand.Seed(42)
+
+ var (
+ user = &User{
+ Name: `tester`,
+ Password: `$2a$10$9XMRfqpnzY2421fwYm5dd.CidJf7dHHWIESeeNGXuajHRf.Lqzy7a`, // s3cret
+
+ }
+ env = &Environment{
+ Users: map[string]*User{
+ user.Name: user,
+ },
+ }
+ k = &Karajo{
+ env: env,
+ sm: newSessionManager(),
+ }
+ )
+
+ type testCase struct {
+ expSM *sessionManager
+ desc string
+ name string
+ pass string
+ expError string
+ expResBody string
+ expResHeader string
+ }
+
+ var cases = []testCase{{
+ desc: `with empty name and password`,
+ expError: errAuthLogin.Error(),
+ }, {
+ desc: `with empty password`,
+ name: user.Name,
+ expError: errAuthLogin.Message,
+ }, {
+ desc: `with unknown user name`,
+ name: `unknown`,
+ pass: `notempty`,
+ expError: errAuthLogin.Message,
+ }, {
+ desc: `with invalid password`,
+ name: user.Name,
+ pass: `invalid`,
+ expError: errAuthLogin.Message,
+ }, {
+ desc: `with valid name and password`,
+ name: user.Name,
+ pass: `s3cret`,
+ expResBody: `{"code":200}`,
+ expResHeader: "HTTP/1.1 200 OK\r\n" +
+ "Connection: close\r\n" +
+ "Set-Cookie: karajo=ASG5Ohg1l0CMEefBrPrV9QtazJoL6uax; Path=/; Max-Age=86400; HttpOnly\r\n\r\n",
+ expSM: &sessionManager{
+ value: map[string]*User{
+ `ASG5Ohg1l0CMEefBrPrV9QtazJoL6uax`: user,
+ },
+ },
+ }}
+
+ var (
+ testRecorder = httptest.NewRecorder()
+ epr = &libhttp.EndpointRequest{
+ HttpWriter: testRecorder,
+ HttpRequest: &http.Request{
+ Form: url.Values{},
+ },
+ }
+
+ c testCase
+ respBody []byte
+ rawResp []byte
+ err error
+ httpResp *http.Response
+ )
+
+ for _, c = range cases {
+ epr.HttpRequest.Form.Set(`name`, c.name)
+ epr.HttpRequest.Form.Set(`password`, c.pass)
+
+ respBody, err = k.apiAuthLogin(epr)
+ if err != nil {
+ test.Assert(t, c.desc+`: error`, c.expError, err.Error())
+ continue
+ }
+
+ test.Assert(t, c.desc+`: respBody`, c.expResBody, string(respBody))
+
+ httpResp = testRecorder.Result()
+ rawResp, err = httputil.DumpResponse(httpResp, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ test.Assert(t, c.desc+`: response header`, c.expResHeader, string(rawResp))
+
+ test.Assert(t, c.desc+`: session manager`, c.expSM, k.sm)
+ }
+}
A karajo_session.go => karajo_session.go +39 -0
@@ 0,0 1,39 @@
+// SPDX-FileCopyrightText: 2023 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package karajo
+
+import (
+ "fmt"
+ "net/http"
+)
+
+const (
+ cookieName = `karajo`
+)
+
+// sessionNew generate and store new session for user.
+func (k *Karajo) sessionNew(w http.ResponseWriter, user *User) (err error) {
+ var (
+ logp = `sessionNew`
+ key string
+ )
+
+ key = k.sm.new(user)
+ if len(key) == 0 {
+ return fmt.Errorf(`%s: failed to generate new session`, logp)
+ }
+
+ var cookie = &http.Cookie{
+ Name: cookieName,
+ Value: key,
+ MaxAge: 86400, // One day in seconds.
+ Path: `/`,
+ Secure: false,
+ HttpOnly: true,
+ }
+
+ http.SetCookie(w, cookie)
+
+ return nil
+}