From a3e878d677ab6ad81cfab762e6cbbe44e9b8398e Mon Sep 17 00:00:00 2001 From: nxshock Date: Thu, 18 Jul 2019 19:10:43 +0500 Subject: [PATCH] Upload code --- backend.go | 10 +++ consts.go | 33 +++++++++ errors.go | 26 +++++++ handlers.go | 175 +++++++++++++++++++++++++++++++++++++++++++++ main.go | 20 ++++++ matrix/enums.go | 89 +++++++++++++++++++++++ matrix/errors.go | 52 ++++++++++++++ matrix/replies.go | 70 ++++++++++++++++++ matrix/requests.go | 66 +++++++++++++++++ matrix/sync.go | 141 ++++++++++++++++++++++++++++++++++++ matrix/types.go | 30 ++++++++ server.go | 39 ++++++++++ tokenutils.go | 34 +++++++++ users.go | 121 +++++++++++++++++++++++++++++++ 14 files changed, 906 insertions(+) create mode 100644 backend.go create mode 100644 consts.go create mode 100644 errors.go create mode 100644 handlers.go create mode 100644 main.go create mode 100644 matrix/enums.go create mode 100644 matrix/errors.go create mode 100644 matrix/replies.go create mode 100644 matrix/requests.go create mode 100644 matrix/sync.go create mode 100644 matrix/types.go create mode 100644 server.go create mode 100644 tokenutils.go create mode 100644 users.go diff --git a/backend.go b/backend.go new file mode 100644 index 0000000..a662526 --- /dev/null +++ b/backend.go @@ -0,0 +1,10 @@ +package main + +import "github.com/nxshock/go-lightning/matrix" + +type Backend interface { + Register(username, password, device string) (token string, error *matrix.ApiError) + Login(username, password, device string) (token string, err *matrix.ApiError) + Logout(token string) *matrix.ApiError + Sync(token string, request matrix.SyncRequest) (response *matrix.SyncReply, err *matrix.ApiError) +} diff --git a/consts.go b/consts.go new file mode 100644 index 0000000..71708f5 --- /dev/null +++ b/consts.go @@ -0,0 +1,33 @@ +package main + +const ( + Version = "r0.5.0" + DefaultTokenSize = 16 +) + +// https://matrix.org/docs/spec/client_server/latest#authentication-types +type authenticationType string + +const ( + M_LOGIN_PASSWORD authenticationType = "m.login.password" + M_LOGIN_RECAPTCHA authenticationType = "m.login.recaptcha" + M_LOGIN_OAUTH2 authenticationType = "m.login.oauth2" + M_LOGIN_EMAIL_IDENTITY authenticationType = "m.login.email.identity" + M_LOGIN_MSISDN authenticationType = "m.login.msisdn" + M_LOGIN_TOKEN authenticationType = "m.login.token" + M_LOGIN_DUMMY authenticationType = "m.login.dummy" +) + +// https://matrix.org/docs/spec/client_server/latest#identifier-types +type identifierType string + +const ( + // https://matrix.org/docs/spec/client_server/latest#matrix-user-id + M_ID_USER identifierType = "m.id.user" + + // https://matrix.org/docs/spec/client_server/latest#third-party-id + M_ID_THIRDPARTY identifierType = "m.id.thirdparty" + + // https://matrix.org/docs/spec/client_server/latest#phone-number + M_ID_PHONE identifierType = "m.id.phone" +) diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..feff407 --- /dev/null +++ b/errors.go @@ -0,0 +1,26 @@ +package main + +import ( + "net/http" + + "github.com/nxshock/go-lightning/matrix" +) + +func errorResponse(w http.ResponseWriter, code matrix.ApiError, httpCode int, message string) { + w.Header().Set("Content-Type", "application/json") + + if message != "" { + code.Message = message + } + + w.WriteHeader(httpCode) + w.Write(code.JSON()) +} + +func NewError(code matrix.ApiError, message string) *matrix.ApiError { + if message != "" { + code.Message = message + } + + return &code +} diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..b94be41 --- /dev/null +++ b/handlers.go @@ -0,0 +1,175 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" + + "github.com/nxshock/go-lightning/matrix" +) + +func RootHandler(w http.ResponseWriter, r *http.Request) { + log.Println(r.RequestURI) +} + +// https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions +func VersionHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + errorResponse(w, matrix.M_UNKNOWN, http.StatusBadRequest, "wrong method: "+r.Method) + return + } + + response := matrix.VersionsReply{Versions: []string{Version}} + sendJsonResponse(w, http.StatusOK, response) +} + +// https://matrix.org/docs/spec/client_server/latest#login +func LoginHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + // https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login + case "GET": + { + type LoginFlow struct { + Type string `json:"type"` + } + + type Response struct { + Flows []LoginFlow `json:"flows"` + } + + response := Response{ + Flows: []LoginFlow{ + LoginFlow{Type: string(M_LOGIN_PASSWORD)}}} + + sendJsonResponse(w, http.StatusOK, response) + } + + // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login + case "POST": + { + var request matrix.LoginRequest + getRequest(r, &request) // TODO: handle error + + // delete start "@" if presents + if strings.HasPrefix(request.Identifier.User, "@") { + request.Identifier.User = strings.TrimPrefix(request.Identifier.User, "@") + } + + token, apiErr := server.Backend.Login(request.Identifier.User, request.Password, request.DeviceID) + if apiErr != nil { + errorResponse(w, *apiErr, http.StatusForbidden, "") + return + } + + response := matrix.LoginReply{ + UserID: request.Identifier.User, + AccessToken: token} + + sendJsonResponse(w, http.StatusOK, response) + } + } +} + +// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-logout +func LogoutHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + errorResponse(w, matrix.M_UNKNOWN, http.StatusBadRequest, "wrong method: "+r.Method) + return + } + + token := getTokenFromResponse(r) + + if token == "" { + errorResponse(w, matrix.M_MISSING_TOKEN, http.StatusBadRequest, "") + return + } + + apiErr := server.Backend.Logout(token) + if apiErr != nil { + errorResponse(w, *apiErr, http.StatusBadRequest, "") // TODO: check code + return + } + + sendJsonResponse(w, http.StatusOK, struct{}{}) +} + +// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-register +func RegisterHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + errorResponse(w, matrix.M_UNKNOWN, http.StatusBadRequest, "wrong method: "+r.Method) + return + } + + kind := r.FormValue("kind") + if kind != "user" { + errorResponse(w, matrix.M_UNKNOWN, http.StatusBadRequest, "wrong kind: "+kind) + return + } + + var request matrix.RegisterRequest + getRequest(r, &request) // TODO: handle error + + token, apiErr := server.Backend.Register(request.Username, request.Password, request.DeviceID) + if apiErr != nil { + errorResponse(w, *apiErr, http.StatusBadRequest, "") + return + } + + var response matrix.RegisterResponse + response.UserID = "@" + request.Username + response.DeviceID = request.DeviceID + response.AccessToken = token + + sendJsonResponse(w, http.StatusOK, response) +} + +// https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-sync +func SyncHandler(w http.ResponseWriter, r *http.Request) { + var request matrix.SyncRequest + request.Filter = r.FormValue("filter") + + timeout, err := strconv.Atoi(r.FormValue("timeout")) + if err != nil { + errorResponse(w, matrix.M_UNKNOWN, http.StatusBadRequest, "timeout parse failes") + return + } + request.Timeout = timeout + //log.Printf("%+v", request) + + token := getTokenFromResponse(r) + if token == "" { + errorResponse(w, matrix.M_MISSING_TOKEN, http.StatusBadRequest, "") + return + } + + response, _ := server.Backend.Sync(token, request) // TODO: handle error + + response.NextBatch = "123" + response.Rooms = matrix.RoomsSyncReply{} + + sendJsonResponse(w, http.StatusOK, response) +} + +func sendJsonResponse(w http.ResponseWriter, httpStatus int, data interface{}) error { + b, err := json.Marshal(data) + if err != nil { + return err + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(httpStatus) + _, err = w.Write(b) // TODO: handle n + return err +} + +func getRequest(r *http.Request, request interface{}) error { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + + return json.Unmarshal(b, request) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..0b58403 --- /dev/null +++ b/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "log" +) + +var ( + server *Server +) + +func init() { + server = New() + server.Address = "localhost" + server.Backend = NewMemoryBackend() + server.Backend.Register("andrew", "1", "") +} + +func main() { + log.Println(server.Run()) +} diff --git a/matrix/enums.go b/matrix/enums.go new file mode 100644 index 0000000..d19f2c6 --- /dev/null +++ b/matrix/enums.go @@ -0,0 +1,89 @@ +package matrix + +type Membership string + +const ( + MembershipInvite Membership = "invite" + MembershipJoin Membership = "join" + MembershipKnock Membership = "knock" + MembershipLeave Membership = "leave" + MembershipBan Membership = "ban" +) + +type SetPresence string + +const ( + SetPresenceOffline SetPresence = "offline" + SetPresenceOnline SetPresence = "online" + SetPresenceUnavailable SetPresence = "unavailable" +) + +type JoinRule string + +const ( + JoinRulePublic JoinRule = "public" + JoinRuleKnock JoinRule = "knock" + JoinRuleInvite JoinRule = "invite" + JoinRulePrivate JoinRule = "private" +) + +// https://matrix.org/docs/spec/client_server/r0.4.0.html#m-room-message-msgtypes +type MessageType string + +const ( + MessageTypeText MessageType = "m.text" // https://matrix.org/docs/spec/client_server/r0.4.0.html#m-text + MessageTypeEmote MessageType = "m.emote" // https://matrix.org/docs/spec/client_server/r0.4.0.html#m-emote + MessageTypeNotice MessageType = "m.notice" // https://matrix.org/docs/spec/client_server/r0.4.0.html#m-notice + MessageTypeImage MessageType = "m.image" // https://matrix.org/docs/spec/client_server/r0.4.0.html#m-image + MessageTypeFile MessageType = "m.file" // https://matrix.org/docs/spec/client_server/r0.4.0.html#m-file + MessageTypeVideo MessageType = "m.video" // https://matrix.org/docs/spec/client_server/r0.4.0.html#m-video + MessageTypeAudio MessageType = "m.audio" // https://matrix.org/docs/spec/client_server/r0.4.0.html#m-audio + MessageTypeLocation MessageType = "m.location" // https://matrix.org/docs/spec/client_server/r0.4.0.html#m-location + +) + +// https://matrix.org/docs/spec/client_server/r0.4.0.html#id207 +type IdentifierType string + +const ( + IdentifierTypeUser IdentifierType = "m.id.user" // https://matrix.org/docs/spec/client_server/r0.4.0.html#id208 + IdentifierTypeThirdparty IdentifierType = "m.id.thirdparty" // https://matrix.org/docs/spec/client_server/r0.4.0.html#id209 + IdentifierTypePhone IdentifierType = "m.id.phone" // https://matrix.org/docs/spec/client_server/r0.4.0.html#id210 +) + +// Authentication types +// https://matrix.org/docs/spec/client_server/r0.4.0.html#id198 +type AuthenticationType string + +const ( + // Password-based + // https://matrix.org/docs/spec/client_server/r0.4.0.html#id199 + AuthenticationTypePassword AuthenticationType = "m.login.password" + + // Google ReCaptcha + // https://matrix.org/docs/spec/client_server/r0.4.0.html#id200 + AuthenticationTypeRecaptcha AuthenticationType = "m.login.recaptcha" + + // OAuth2-based + // https://matrix.org/docs/spec/client_server/r0.4.0.html#id202 + AuthenticationTypeOauth2 AuthenticationType = "m.login.oauth2" + + // Email-based (identity server) + // https://matrix.org/docs/spec/client_server/r0.4.0.html#id203 + AuthenticationTypeEmail AuthenticationType = "m.login.email.identity" + + // Token-based + // https://matrix.org/docs/spec/client_server/r0.4.0.html#id201 + AuthenticationTypeToken AuthenticationType = "m.login.token" + + // Dummy Auth + // https://matrix.org/docs/spec/client_server/r0.4.0.html#id204 + AuthenticationTypeDummy AuthenticationType = "m.login.dummy" +) + +type VisibilityType string + +const ( + VisibilityTypePrivate = "private" + VisibilityTypePublic = "public" +) diff --git a/matrix/errors.go b/matrix/errors.go new file mode 100644 index 0000000..6133f99 --- /dev/null +++ b/matrix/errors.go @@ -0,0 +1,52 @@ +package matrix + +import ( + "encoding/json" +) + +type ApiError struct { + Code string `json:"errcode"` + Message string `json:"error"` +} + +var ( + // https://matrix.org/docs/spec/client_server/latest#api-standards + + M_FORBIDDEN = ApiError{"M_FORBIDDEN", ""} // Forbidden access, e.g. joining a room without permission, failed login. + M_UNKNOWN_TOKEN = ApiError{"M_UNKNOWN_TOKEN", ""} // The access token specified was not recognised. + M_MISSING_TOKEN = ApiError{"M_MISSING_TOKEN", ""} // No access token was specified for the request. + M_BAD_JSON = ApiError{"M_BAD_JSON", ""} // Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. + M_NOT_JSON = ApiError{"M_NOT_JSON", ""} // Request did not contain valid JSON. + M_NOT_FOUND = ApiError{"M_NOT_FOUND", ""} // No resource was found for this request. + M_LIMIT_EXCEEDED = ApiError{"M_LIMIT_EXCEEDED", ""} // Too many requests have been sent in a short period of time. Wait a while then try again. + M_UNKNOWN = ApiError{"M_UNKNOWN", ""} // An unknown error has occurred. + + M_UNRECOGNIZED = ApiError{"M_UNRECOGNIZED", ""} // The server did not understand the request. + M_UNAUTHORIZED = ApiError{"M_UNAUTHORIZED", ""} // The request was not correctly authorized. Usually due to login failures. + M_USER_IN_USE = ApiError{"M_USER_IN_USE", ""} // Encountered when trying to register a user ID which has been taken. + M_INVALID_USERNAME = ApiError{"M_INVALID_USERNAME", ""} // Encountered when trying to register a user ID which is not valid. + M_ROOM_IN_USE = ApiError{"M_ROOM_IN_USE", ""} // Sent when the room alias given to the createRoom API is already in use. + M_INVALID_ROOM_STATE = ApiError{"M_INVALID_ROOM_STATE", ""} // Sent when the initial state given to the createRoom API is invalid. + M_THREEPID_IN_USE = ApiError{"M_THREEPID_IN_USE", ""} // Sent when a threepid given to an API cannot be used because the same threepid is already in use. + M_THREEPID_NOT_FOUND = ApiError{"M_THREEPID_NOT_FOUND", ""} // Sent when a threepid given to an API cannot be used because no record matching the threepid was found. + M_THREEPID_AUTH_FAILED = ApiError{"M_THREEPID_AUTH_FAILED", ""} // Authentication could not be performed on the third party identifier. + M_THREEPID_DENIED = ApiError{"M_THREEPID_DENIED", ""} // The server does not permit this third party identifier. This may happen if the server only permits, for example, email addresses from a particular domain. + M_SERVER_NOT_TRUSTED = ApiError{"M_SERVER_NOT_TRUSTED", ""} // The client's request used a third party server, eg. identity server, that this server does not trust. + M_UNSUPPORTED_ROOM_VERSION = ApiError{"M_UNSUPPORTED_ROOM_VERSION", ""} // The client's request to create a room used a room version that the server does not support. + M_INCOMPATIBLE_ROOM_VERSION = ApiError{"M_INCOMPATIBLE_ROOM_VERSION", ""} // The client attempted to join a room that has a version the server does not support. Inspect the room_version property of the error response for the room's version. + M_BAD_STATE = ApiError{"M_BAD_STATE", ""} // The state change requested cannot be performed, such as attempting to unban a user who is not banned. + M_GUEST_ACCESS_FORBIDDEN = ApiError{"M_GUEST_ACCESS_FORBIDDEN", ""} // The room or resource does not permit guests to access it. + M_CAPTCHA_NEEDED = ApiError{"M_CAPTCHA_NEEDED", ""} // A Captcha is required to complete the request. + M_CAPTCHA_INVALID = ApiError{"M_CAPTCHA_INVALID", ""} // The Captcha provided did not match what was expected. + M_MISSING_PARAM = ApiError{"M_MISSING_PARAM", ""} // A required parameter was missing from the request. + M_INVALID_PARAM = ApiError{"M_INVALID_PARAM", ""} // A parameter that was specified has the wrong value. For example, the server expected an integer and instead received a string. + M_TOO_LARGE = ApiError{"M_TOO_LARGE", ""} // The request or entity was too large. + M_EXCLUSIVE = ApiError{"M_EXCLUSIVE", ""} // The resource being requested is reserved by an application service, or the application service making the request has not created the resource. + M_RESOURCE_LIMIT_EXCEEDED = ApiError{"M_RESOURCE_LIMIT_EXCEEDED", ""} // The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example, a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much memory or disk space. The error MUST have an admin_contact field to provide the user receiving the error a place to reach out to. Typically, this error will appear on routes which attempt to modify state (eg: sending messages, account data, etc) and not routes which only read state (eg: /sync, get account data, etc). + M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = ApiError{"M_CANNOT_LEAVE_SERVER_NOTICE_ROOM", ""} // The user is unable to reject an invite to join the server notices room. See the Server Notices module for more information. +) + +func (apiError ApiError) JSON() []byte { + b, _ := json.Marshal(apiError) // TODO: error handler? + return b +} diff --git a/matrix/replies.go b/matrix/replies.go new file mode 100644 index 0000000..cdff185 --- /dev/null +++ b/matrix/replies.go @@ -0,0 +1,70 @@ +package matrix + +type JoinedRoomReply struct { + JoinedRooms []string `json:"joined_rooms"` +} + +// SendMessageReply represents reply for send message command +// https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey +type SendMessageReply struct { + EventID string `json:"event_id"` // A unique identifier for the event. +} + +// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-createroom +type CreateRoomReply struct { + RoomID string `json:"room_id"` // Information about the newly created room. +} + +type GetLoginReply struct { + Flows []Flow // The homeserver's supported login types +} + +type LoginReply struct { + AccessToken string `json:"access_token"` + HomeServer string `json:"home_server,omitempty"` // TODO: check api + UserID string `json:"user_id"` +} + +type SyncReply struct { + NextBatch string `json:"next_batch"` // Required. The batch token to supply in the since param of the next /sync request. + Rooms RoomsSyncReply `json:"rooms"` // Updates to rooms. + Presence Presence `json:"presence"` // The updates to the presence status of other users. + AccountData AccountData `json:"account_data"` // The global private data created by this user. + ToDevice ToDevice `json:"to_device"` // Information on the send-to-device messages for the client device, as defined in Send-to-Device messaging. + DeviceLists DeviceLists `json:"device_lists"` // Information on end-to-end device updates, as specified in End-to-end encryption. + DeviceOneTimeKeysCount map[string]int `json:"device_one_time_keys_count"` // Information on end-to-end encryption keys, as specified in End-to-end encryption. +} + +type RoomsSyncReply struct { + Join map[string]JoinedRoom `json:"join"` // The rooms that the user has joined. + Invite map[string]InvitedRoom `json:"invite"` // The rooms that the user has been invited to. + Leave map[string]LeftRoom `json:"leave"` // The rooms that the user has left or been banned from. +} + +// https://matrix.org/docs/spec/client_server/r0.4.0.html#id276 +type JoinRoomReply struct { + RoomID string `json:"room_id"` // The joined room ID must be returned in the room_id field. +} + +// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-versions +type VersionsReply struct { + Versions []string `json:"versions"` // The supported versions. + UnstableFeatures map[string]bool `json:"unstable_features,omitempty"` +} + +// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-account-whoami +type WhoAmIReply struct { + UserID string `json:"user_id"` // Required. The user id that owns the access token. +} + +// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-rooms-roomid-members +type MembersReply struct { + Chunk []MemberEvent `json:"chunk"` +} + +// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-register +type RegisterResponse struct { + UserID string `json:"user_id"` // Required. The fully-qualified Matrix user ID (MXID) that has been registered. Any user ID returned by this API must conform to the grammar given in the Matrix specification. + AccessToken string `json:"access_token,omitempty"` // An access token for the account. This access token can then be used to authorize other requests. Required if the inhibit_login option is false. + DeviceID string `json:"device_id,omitempty"` // ID of the registered device. Will be the same as the corresponding parameter in the request, if one was specified. Required if the inhibit_login option is false. +} diff --git a/matrix/requests.go b/matrix/requests.go new file mode 100644 index 0000000..6852776 --- /dev/null +++ b/matrix/requests.go @@ -0,0 +1,66 @@ +package matrix + +// LoginRequest represents login request +// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-login +type LoginRequest struct { + Type AuthenticationType `json:"type"` // Required. The login type being used. One of: ["m.login.password", "m.login.token"] + Identifier UserIdentifier `json:"identifier"` // Identification information for the user. + Password string `json:"password,omitempty"` // Required when type is m.login.password. The user's password. + Token string `json:"token,omitempty"` // Required when type is m.login.token. Part of Token-based login. + DeviceID string `json:"device_id,omitempty"` // ID of the client device. If this does not correspond to a known client device, a new device will be created. The server will auto-generate a device_id if this is not specified. + InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"` // A display name to assign to the newly-created device. Ignored if device_id corresponds to a known device. +} + +// UserIdentifier represents user identifier object +// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-login +type UserIdentifier struct { + Type IdentifierType `json:"type"` // Required. The type of identification. See Identifier types for supported values and additional property descriptions. + User string `json:"user,omitempty"` // The fully qualified user ID or just local part of the user ID, to log in. + Medium string `json:"medium,omitempty"` // When logging in using a third party identifier, the medium of the identifier. Must be 'email'. + Address string `json:"address,omitempty"` // Third party identifier for the user. + Country string `json:"country,omitempty"` + Phone string `json:"phone,omitempty"` +} + +// CreateRoomRequest represents room creation request +// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-createroom +type CreateRoomRequest struct { + Visibility VisibilityType `json:"visibility,omitempty"` + RoomAliasName string `json:"room_alias_name,omitempty"` + Name string `json:"name,omitempty"` + Topic string `json:"topic,omitempty"` + Invite []string `json:"invite,omitempty"` + Invite3pids []Invite3pid `json:"invite_3pid,omitempty"` + RoomVersion string `json:"room_version,omitempty"` + // TODO: проверить тип + // CreationContent CreationContentType `json:"creation_content,omitempty"` + InitialState []StateEvent `json:"initial_state,omitempty"` + Preset string `json:"preset,omitempty"` // TODO: проверить тип + IsDirect bool `json:"is_direct,omitempty"` + // PowerLevelContentOverride `json:"power_level_content_override"` +} + +type SendMessageRequest struct { + Body string `json:"body"` // Required. The textual representation of this message. + MessageType MessageType `json:"msgtype"` // Required. The type of message, e.g. m.image, m.text + RelatesTo MRelatesTo `json:"m.relates_to,omitempty"` +} + +// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-register +type RegisterRequest struct { + Auth AuthenticationData `json:"auth"` // Additional authentication information for the user-interactive authentication API. Note that this information is not used to define how the registered user should be authenticated, but is instead used to authenticate the register call itself. + BindEmail bool `json:"bind_email"` // If true, the server binds the email used for authentication to the Matrix ID with the identity server. + BindMsisdn bool `json:"bind_msisdn"` // If true, the server binds the phone number used for authentication to the Matrix ID with the identity server. + Username string `json:"username"` // The basis for the localpart of the desired Matrix ID. If omitted, the homeserver MUST generate a Matrix ID local part. + Password string `json:"password"` // The desired password for the account. + DeviceID string `json:"device_id"` // ID of the client device. If this does not correspond to a known client device, a new device will be created. The server will auto-generate a device_id if this is not specified. + InitialDeviceDisplayName string `json:"initial_device_display_name"` // A display name to assign to the newly-created device. Ignored if device_id corresponds to a known device. + InhibitLogin bool `json:"inhibit_login"` // If true, an access_token and device_id should not be returned from this call, therefore preventing an automatic login. Defaults to false. + +} + +// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-register +type AuthenticationData struct { + Type string `json:"type"` // Required. The login type that the client is attempting to complete. + Session string `json:"session,omitempty"` // The value of the session key given by the homeserver. +} diff --git a/matrix/sync.go b/matrix/sync.go new file mode 100644 index 0000000..6b3074d --- /dev/null +++ b/matrix/sync.go @@ -0,0 +1,141 @@ +package matrix + +import ( + "encoding/json" +) + +// https://matrix.org/docs/spec/client_server/r0.4.0.html#id242 +type SyncRequest struct { + Filter string `url:"filter,omitempty"` // The ID of a filter created using the filter API or a filter JSON object encoded as a string. The server will detect whether it is an ID or a JSON object by whether the first character is a "{" open brace. Passing the JSON inline is best suited to one off requests. Creating a filter using the filter API is recommended for clients that reuse the same filter multiple times, for example in long poll requests. + Since string `url:"since,omitempty"` // A point in time to continue a sync from. + FullState bool `url:"full_state,omitempty"` // Controls whether to include the full state for all rooms the user is a member of. + SetPresence SetPresence `url:"set_presence,omitempty"` // Controls whether the client is automatically marked as online by polling this API. If this parameter is omitted then the client is automatically marked as online when it uses this API. Otherwise if the parameter is set to "offline" then the client is not marked as being online when it uses this API. When set to "unavailable", the client is marked as being idle. One of: ["offline", "online", "unavailable"] + Timeout int `url:"timeout,omitempty"` // The maximum time to wait, in milliseconds, before returning this request. If no events (or other data) become available before this time elapses, the server will return a response with empty fields. +} + +type JoinedRoom struct { + State State `json:"state"` // Updates to the state, between the time indicated by the since parameter, and the start of the timeline (or all state up to the start of the timeline, if since is not given, or full_state is true). + Timeline Timeline `json:"timeline"` // The timeline of messages and state changes in the room. + Ephemeral Ephemeral `json:"ephemeral"` // The ephemeral events in the room that aren't recorded in the timeline or state of the room. e.g. typing. + AccountData AccountData `json:"account_data"` // The private data that this user has attached to this room. + UnreadNotifications UnreadNotificationCounts `json:"unread_notifications"` // Counts of unread notifications for this room +} + +type Ephemeral struct { + Events []Event `json:"events"` // List of events. +} + +type UnreadNotificationCounts struct { + HighlightCount int `json:"highlight_count"` // The number of unread notifications for this room with the highlight flag set + NotificationCount int `json:"notification_count"` // The total number of unread notifications for this room +} + +type InvitedRoom struct { + InviteState InviteState `json:"invite_state"` // The state of a room that the user has been invited to. These state events may only have the sender, type, state_key and content keys present. These events do not replace any state that the client already has for the room, for example if the client has archived the room. Instead the client should keep two separate copies of the state: the one from the invite_state and one from the archived state. If the client joins the room then the current state will be given as a delta against the archived state not the invite_state. +} + +type InviteState struct { + Events []StrippedState `json:"events"` // The StrippedState events that form the invite state. +} + +type StrippedState struct { + // TODO: в документации EventContent, хотя вроде сервер выдаёт json.RawMessage + Content json.RawMessage `json:"content"` // Required. The content for the event. + StateKey string `json:"state_key"` // Required. The state_key for the event. + Type string `json:"type"` // Required. The type for the event. + Sender string `json:"sender"` // Required. The sender for the event. +} + +type LeftRoom struct { + State State `json:"state"` // The state updates for the room up to the start of the timeline. + Timeline Timeline `json:"timeline"` // The timeline of messages and state changes in the room up to the point when the user left. + AccountData AccountData `json:"account_data"` // The private data that this user has attached to this room. +} + +type State struct { + events []StateEvent `json:"events"` // List of events. +} + +type StateEvent struct { + // TODO: object? + Content json.RawMessage `json:"content"` // Required. The fields in this object will vary depending on the type of event. When interacting with the REST API, this is the HTTP body. + Type string `json:"type"` // Required. The type of event. This SHOULD be namespaced similar to Java package naming conventions e.g. 'com.example.subdomain.event.type' + EventID string `json:"event_id"` // Required. The globally unique event identifier. + Sender string `json:"sender"` // Required. Contains the fully-qualified ID of the user who sent this event. + OriginServerTs int `json:"origin_server_ts"` // Required. Timestamp in milliseconds on originating homeserver when this event was sent. + Unsigned UnsignedData `json:"unsigned"` // Contains optional extra information about the event. + PrevContent EventContent `json:"prev_content"` // Optional. The previous content for this event. If there is no previous content, this key will be missing. + StateKey string `json:"state_key"` // Required. A unique key which defines the overwriting semantics for this piece of room state. This value is often a zero-length string. The presence of this key makes this event a State Event. State keys starting with an @ are reserved for referencing user IDs, such as room members. With the exception of a few events, state events set with a given user's ID as the state key MUST only be set by that user. +} + +type Timeline struct { + Events []RoomEvent `json:"events"` // List of events. + Limited bool `json:"limited"` // True if the number of events returned was limited by the limit on the filter. + PrevBatch string `json:"prev_batch"` // A token that can be supplied to the from parameter of the rooms/{roomId}/messages endpoint. +} + +type RoomEvent struct { + // TODO: object + Content json.RawMessage `json:"content"` // Required. The fields in this object will vary depending on the type of event. When interacting with the REST API, this is the HTTP body. + Type string `json:"type"` // Required. The type of event. This SHOULD be namespaced similar to Java package naming conventions e.g. 'com.example.subdomain.event.type' + EventID string `json:"event_id"` // Required. The globally unique event identifier. + Sender string `json:"sender"` // Required. Contains the fully-qualified ID of the user who sent this event. + OriginServerTs int64 `json:"origin_server_ts"` // Required. Timestamp in milliseconds on originating homeserver when this event was sent. + Unsigned UnsignedData `json:"unsigned"` // Contains optional extra information about the event. +} + +type UnsignedData struct { + Age int `json:"age"` // The time in milliseconds that has elapsed since the event was sent. This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers is out of sync, which can cause the age to either be negative or greater than it actually is. + RedactedBecause Event `json:"redacted_because"` // Optional. The event that redacted this event, if any. + TransactionID string `json:"transaction_id"` // The client-supplied transaction ID, if the client being given the event is the same one which sent it. +} + +type Presence struct { + events []Event `json:"events"` // List of events. +} + +type AccountData struct { + Events []Event `json:"events"` // List of events. +} + +type Event struct { + // TODO: object + Content json.RawMessage `json:"content"` // Required. The fields in this object will vary depending on the type of event. When interacting with the REST API, this is the HTTP body. + Type string `json:"type"` // Required. The type of event. This SHOULD be namespaced similar to Java package naming conventions e.g. 'com.example.subdomain.event.type' +} + +type EventContent struct { + AvatarURL string `json:"avatar_url"` // The avatar URL for this user, if any. This is added by the homeserver. + // TODO: string or null + DisplayName string `json:"displayname"` // The display name for this user, if any. This is added by the homeserver. + Membership Membership `json:"membership"` // Required. The membership state of the user. One of: ["invite", "join", "knock", "leave", "ban"] + IsDirect bool `json:"is_direct"` // Flag indicating if the room containing this event was created with the intention of being a direct chat. See Direct Messaging. + ThirdPartyInvite Invite `json:"third_party_invite"` // + Unsigned UnsignedData `json:"unsigned"` // Contains optional extra information about the event. +} + +type Invite struct { + DisplayName string `json:"display_name"` // Required. A name which can be displayed to represent the user instead of their third party identifier + Signed signed `json:"signed"` // Required. A block of content which has been signed, which servers can use to verify the event. Clients should ignore this. +} + +type signed struct { + Mxid string `json:"mxid"` // Required. The invited matrix user ID. Must be equal to the user_id property of the event. + // TODO: + // Signatures Signatures `json:"signatures"` // Required. A single signature from the verifying server, in the format specified by the Signing Events section of the server-server API. + Token string `json:"token"` // Required. The token property of the containing third_party_invite object. +} + +// TODO: проверить правильность выбора типа +type ToDevice struct { + events []Event `json:"events` // List of send-to-device messages +} + +type DeviceLists struct { + Changed []string `json:"changed"` // List of users who have updated their device identity keys, or who now share an encrypted room with the client since the previous sync response. + Left []string `json:"left"` // List of users with whom we do not share any encrypted rooms anymore since the previous sync response. +} + +type Flow struct { + Type AuthenticationType `json:"type"` +} diff --git a/matrix/types.go b/matrix/types.go new file mode 100644 index 0000000..e32c9c8 --- /dev/null +++ b/matrix/types.go @@ -0,0 +1,30 @@ +package matrix + +type MRelatesTo struct { + InReplyTo MInReplyTo `json:"m.in_reply_to"` +} + +type MInReplyTo struct { + EventID string `json:"event_id"` +} + +// Invite3pid represents third party IDs to invite into the room +// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-createroom +type Invite3pid struct { + IDServer string `json:"id_server"` // Required. The hostname+port of the identity server which should be used for third party identifier lookups. + Medium string `json:"medium"` // Required. The kind of address being passed in the address field, for example email. + Address string `json:"address"` // Required. The invitee's third party identifier. +} + +// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-rooms-roomid-members +type MemberEvent struct { + Content EventContent `json:"content"` // Required. + Type string `json:"type"` //Required. Must be 'm.room.member'. + EventID string `json:"event_id"` // Required. The globally unique event identifier. + Sender string `json:"sender"` // Required. Contains the fully-qualified ID of the user who sent this event. + OriginServerTs int `json:"origin_server_ts"` // Required. Timestamp in milliseconds on originating homeserver when this event was sent. + Unsigned UnsignedData `json:"unsigned"` // Contains optional extra information about the event. + RoomID string `json:"room_id"` // Required. The ID of the room associated with this event. Will not be present on events that arrive through /sync, despite being required everywhere else. + PrevContent EventContent `json:"prev_content"` // Optional. The previous content for this event. If there is no previous content, this key will be missing. + StateKey string `json:"state_key"` // Required. The user_id this membership event relates to. In all cases except for when membership is join, the user ID sending the event does not need to match the user ID in the state_key, unlike other events. Regular authorisation rules still apply. +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..5812fa9 --- /dev/null +++ b/server.go @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +type Server struct { + httpServer *http.Server + router *mux.Router + + Address string + Backend Backend +} + +func New() *Server { + router := mux.NewRouter() + router.HandleFunc("/_matrix/client/versions", VersionHandler) + router.HandleFunc("/_matrix/client/r0/login", LoginHandler) + router.HandleFunc("/_matrix/client/r0/logout", LogoutHandler) + router.HandleFunc("/_matrix/client/r0/register", RegisterHandler) + router.HandleFunc("/_matrix/client/r0/sync", SyncHandler) + router.HandleFunc("/", RootHandler) + + httpServer := new(http.Server) + httpServer.Addr = ":80" + httpServer.Handler = router + + server := &Server{ + httpServer: httpServer, + router: router} + + return server +} + +func (server *Server) Run() error { + return server.httpServer.ListenAndServe() // TODO: custom port +} diff --git a/tokenutils.go b/tokenutils.go new file mode 100644 index 0000000..b42eee5 --- /dev/null +++ b/tokenutils.go @@ -0,0 +1,34 @@ +package main + +import ( + "crypto/rand" + "fmt" + "net/http" + "strings" +) + +// newToken returns new generated token with specified length +func newToken(size int) string { + b := make([]byte, size) + rand.Read(b) + + return fmt.Sprintf("%x", b) +} + +// getTokenFromResponse returns token from request. +func getTokenFromResponse(r *http.Request) string { + const prefix = "Bearer " + + auth, ok := r.Header["Authorization"] + if !ok { + return "" + } + + for _, v := range auth { + if strings.HasPrefix(v, prefix) { + return strings.TrimPrefix(v, prefix) + } + } + + return "" +} diff --git a/users.go b/users.go new file mode 100644 index 0000000..50d684e --- /dev/null +++ b/users.go @@ -0,0 +1,121 @@ +package main + +import ( + "encoding/json" + "log" + "os" + "sync" + + "github.com/nxshock/go-lightning/matrix" +) + +var first bool + +type MemoryBackend struct { + data map[string]*User + mutex sync.Mutex // TODO: replace with RW mutex +} + +type User struct { + Password string + Tokens map[string]Token +} + +type Token struct { + Device string +} + +func NewMemoryBackend() *MemoryBackend { + return &MemoryBackend{data: make(map[string]*User)} +} + +func (memoryBackend MemoryBackend) Register(username, password, device string) (token string, err *matrix.ApiError) { + memoryBackend.mutex.Lock() + defer memoryBackend.mutex.Unlock() + + if _, ok := memoryBackend.data[username]; ok { + return "", NewError(matrix.M_USER_IN_USE, "trying to register a user ID which has been taken") + } + + token = newToken(DefaultTokenSize) + + memoryBackend.data[username] = &User{ + Password: password, + Tokens: map[string]Token{ + token: { + Device: device}}} + + return token, nil +} + +func (memoryBackend MemoryBackend) Login(username, password, device string) (token string, err *matrix.ApiError) { + memoryBackend.mutex.Lock() + defer memoryBackend.mutex.Unlock() + + user, ok := memoryBackend.data[username] + if !ok { + return "", NewError(matrix.M_FORBIDDEN, "wrong username") + } + + if user.Password != password { + return "", NewError(matrix.M_FORBIDDEN, "wrong password") + } + + token = newToken(DefaultTokenSize) + + memoryBackend.data[username].Tokens[token] = Token{Device: device} + + return token, nil +} + +func (memoryBackend MemoryBackend) Logout(token string) *matrix.ApiError { + memoryBackend.mutex.Lock() + defer memoryBackend.mutex.Unlock() + + for _, user := range memoryBackend.data { + for userToken, _ := range user.Tokens { + if userToken == token { + delete(user.Tokens, token) + return nil + } + } + } + + return NewError(matrix.M_UNKNOWN_TOKEN, "unknown token") // TODO: create error struct +} + +func (memoryBackend MemoryBackend) Sync(token string, request matrix.SyncRequest) (response *matrix.SyncReply, err *matrix.ApiError) { + memoryBackend.mutex.Lock() + defer memoryBackend.mutex.Unlock() + + log.Println(request) + + if !first { + log.Println(1) + response = &matrix.SyncReply{ + AccountData: matrix.AccountData{ + Events: []matrix.Event{ + matrix.Event{Type: "m.direct", Content: json.RawMessage(`"@vasyo2:localhost":"!room1:localhost"`)}, + }}, + Rooms: matrix.RoomsSyncReply{ + Join: map[string]matrix.JoinedRoom{ + "!room1:localhost": matrix.JoinedRoom{ + Timeline: matrix.Timeline{ + Events: []matrix.RoomEvent{ + matrix.RoomEvent{Type: "m.room.create", Sender: "@vasyo2:localhost"}, + matrix.RoomEvent{Type: "m.room.member", Sender: "@vasyo2:localhost", Content: json.RawMessage(`membership:"join",displayname:"vasyo2"`)}, + }}}}}} + /* InviteState: matrix.InviteState{ + Events: []matrix.StrippedState{ + matrix.StrippedState{Type: "m.room.join_rules", Content: json.RawMessage(`join_rule:"invite"`), Sender: "@vasyo2:" + server.Address}, + matrix.StrippedState{Type: "m.room.member", Content: json.RawMessage(`membership:"join",displayname:"vasyo2"`), Sender: "@vasyo2:" + server.Address}, + matrix.StrippedState{Type: "m.room.member", Content: json.RawMessage(`is_direct:"true",membership:"invite",displayname:"vasyo"`), Sender: "@vasyo2:" + server.Address}, + }}}}}}*/ + first = true + } else { + os.Exit(0) + response = &matrix.SyncReply{} + } + + return response, nil // TODO: implement +}