mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-08 20:10:17 +02:00
Merge branch 'djangomumble' into 'v5.x'
Django Context Mumble Authenticator See merge request allianceauth/allianceauth!1669
This commit is contained in:
commit
ca0ddd7fc7
@ -20,7 +20,8 @@ exclude: |
|
||||
\.mo|
|
||||
swagger\.json|
|
||||
static/(.*)/libs/|
|
||||
telnetlib\.py
|
||||
telnetlib\.py|
|
||||
\.ice
|
||||
)
|
||||
|
||||
repos:
|
||||
|
941
allianceauth/services/modules/mumble/MumbleServer.ice
Normal file
941
allianceauth/services/modules/mumble/MumbleServer.ice
Normal file
@ -0,0 +1,941 @@
|
||||
// Copyright 2022-2023 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
/**
|
||||
*
|
||||
* Information and control of the murmur server. Each server has
|
||||
* one {@link Meta} interface that controls global information, and
|
||||
* each virtual server has a {@link Server} interface.
|
||||
*
|
||||
**/
|
||||
|
||||
#include <Ice/SliceChecksumDict.ice>
|
||||
|
||||
module MumbleServer
|
||||
{
|
||||
|
||||
/** A network address in IPv6 format.
|
||||
**/
|
||||
["python:seq:tuple"] sequence<byte> NetAddress;
|
||||
|
||||
/** A connected user.
|
||||
**/
|
||||
struct User {
|
||||
/** Session ID. This identifies the connection to the server. */
|
||||
int session;
|
||||
/** User ID. -1 if the user is anonymous. */
|
||||
int userid;
|
||||
/** Is user muted by the server? */
|
||||
bool mute;
|
||||
/** Is user deafened by the server? If true, this implies mute. */
|
||||
bool deaf;
|
||||
/** Is the user suppressed by the server? This means the user is not muted, but does not have speech privileges in the current channel. */
|
||||
bool suppress;
|
||||
/** Is the user a priority speaker? */
|
||||
bool prioritySpeaker;
|
||||
/** Is the user self-muted? */
|
||||
bool selfMute;
|
||||
/** Is the user self-deafened? If true, this implies mute. */
|
||||
bool selfDeaf;
|
||||
/** Is the User recording? (This flag is read-only and cannot be changed using setState().) **/
|
||||
bool recording;
|
||||
/** Channel ID the user is in. Matches {@link Channel.id}. */
|
||||
int channel;
|
||||
/** The name of the user. */
|
||||
string name;
|
||||
/** Seconds user has been online. */
|
||||
int onlinesecs;
|
||||
/** Average transmission rate in bytes per second over the last few seconds. */
|
||||
int bytespersec;
|
||||
/** Legacy client version. */
|
||||
int version;
|
||||
/** New client version. (See https://github.com/mumble-voip/mumble/issues/5827) */
|
||||
long version2;
|
||||
/** Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else. */
|
||||
string release;
|
||||
/** Client OS. */
|
||||
string os;
|
||||
/** Client OS Version. */
|
||||
string osversion;
|
||||
/** Plugin Identity. This will be the user's unique ID inside the current game. */
|
||||
string identity;
|
||||
/**
|
||||
Base64-encoded Plugin context. This is a binary blob identifying the game and team the user is on.
|
||||
|
||||
The used Base64 alphabet is the one specified in RFC 2045.
|
||||
|
||||
Before Mumble 1.3.0, this string was not Base64-encoded. This could cause problems for some Ice
|
||||
implementations, such as the .NET implementation.
|
||||
|
||||
If you need the exact string that is used by Mumble, you can get it by Base64-decoding this string.
|
||||
|
||||
If you simply need to detect whether two users are in the same game world, string comparisons will
|
||||
continue to work as before.
|
||||
*/
|
||||
string context;
|
||||
/** User comment. Shown as tooltip for this user. */
|
||||
string comment;
|
||||
/** Client address. */
|
||||
NetAddress address;
|
||||
/** TCP only. True until UDP connectivity is established. */
|
||||
bool tcponly;
|
||||
/** Idle time. This is how many seconds it is since the user last spoke. Other activity is not counted. */
|
||||
int idlesecs;
|
||||
/** UDP Ping Average. This is the average ping for the user via UDP over the duration of the connection. */
|
||||
float udpPing;
|
||||
/** TCP Ping Average. This is the average ping for the user via TCP over the duration of the connection. */
|
||||
float tcpPing;
|
||||
};
|
||||
|
||||
sequence<int> IntList;
|
||||
|
||||
/** A text message between users.
|
||||
**/
|
||||
struct TextMessage {
|
||||
/** Sessions (connected users) who were sent this message. */
|
||||
IntList sessions;
|
||||
/** Channels who were sent this message. */
|
||||
IntList channels;
|
||||
/** Trees of channels who were sent this message. */
|
||||
IntList trees;
|
||||
/** The contents of the message. */
|
||||
string text;
|
||||
};
|
||||
|
||||
/** A channel.
|
||||
**/
|
||||
struct Channel {
|
||||
/** Channel ID. This is unique per channel, and the root channel is always id 0. */
|
||||
int id;
|
||||
/** Name of the channel. There can not be two channels with the same parent that has the same name. */
|
||||
string name;
|
||||
/** ID of parent channel, or -1 if this is the root channel. */
|
||||
int parent;
|
||||
/** List of id of linked channels. */
|
||||
IntList links;
|
||||
/** Description of channel. Shown as tooltip for this channel. */
|
||||
string description;
|
||||
/** Channel is temporary, and will be removed when the last user leaves it. */
|
||||
bool temporary;
|
||||
/** Position of the channel which is used in Client for sorting. */
|
||||
int position;
|
||||
};
|
||||
|
||||
/** A group. Groups are defined per channel, and can inherit members from parent channels.
|
||||
**/
|
||||
struct Group {
|
||||
/** Group name */
|
||||
string name;
|
||||
/** Is this group inherited from a parent channel? Read-only. */
|
||||
bool inherited;
|
||||
/** Does this group inherit members from parent channels? */
|
||||
bool inherit;
|
||||
/** Can subchannels inherit members from this group? */
|
||||
bool inheritable;
|
||||
/** List of users to add to the group. */
|
||||
IntList add;
|
||||
/** List of inherited users to remove from the group. */
|
||||
IntList remove;
|
||||
/** Current members of the group, including inherited members. Read-only. */
|
||||
IntList members;
|
||||
};
|
||||
|
||||
/** Write access to channel control. Implies all other permissions (except Speak). */
|
||||
const int PermissionWrite = 0x01;
|
||||
/** Traverse channel. Without this, a client cannot reach subchannels, no matter which privileges he has there. */
|
||||
const int PermissionTraverse = 0x02;
|
||||
/** Enter channel. */
|
||||
const int PermissionEnter = 0x04;
|
||||
/** Speak in channel. */
|
||||
const int PermissionSpeak = 0x08;
|
||||
/** Whisper to channel. This is different from Speak, so you can set up different permissions. */
|
||||
const int PermissionWhisper = 0x100;
|
||||
/** Mute and deafen other users in this channel. */
|
||||
const int PermissionMuteDeafen = 0x10;
|
||||
/** Move users from channel. You need this permission in both the source and destination channel to move another user. */
|
||||
const int PermissionMove = 0x20;
|
||||
/** Make new channel as a subchannel of this channel. */
|
||||
const int PermissionMakeChannel = 0x40;
|
||||
/** Make new temporary channel as a subchannel of this channel. */
|
||||
const int PermissionMakeTempChannel = 0x400;
|
||||
/** Link this channel. You need this permission in both the source and destination channel to link channels, or in either channel to unlink them. */
|
||||
const int PermissionLinkChannel = 0x80;
|
||||
/** Send text message to channel. */
|
||||
const int PermissionTextMessage = 0x200;
|
||||
/** Kick user from server. Only valid on root channel. */
|
||||
const int PermissionKick = 0x10000;
|
||||
/** Ban user from server. Only valid on root channel. */
|
||||
const int PermissionBan = 0x20000;
|
||||
/** Register and unregister users. Only valid on root channel. */
|
||||
const int PermissionRegister = 0x40000;
|
||||
/** Register and unregister users. Only valid on root channel. */
|
||||
const int PermissionRegisterSelf = 0x80000;
|
||||
/** Reset the comment or avatar of a user. Only valid on root channel. */
|
||||
const int ResetUserContent = 0x100000;
|
||||
|
||||
|
||||
/** Access Control List for a channel. ACLs are defined per channel, and can be inherited from parent channels.
|
||||
**/
|
||||
struct ACL {
|
||||
/** Does the ACL apply to this channel? */
|
||||
bool applyHere;
|
||||
/** Does the ACL apply to subchannels? */
|
||||
bool applySubs;
|
||||
/** Is this ACL inherited from a parent channel? Read-only. */
|
||||
bool inherited;
|
||||
/** ID of user this ACL applies to. -1 if using a group name. */
|
||||
int userid;
|
||||
/** Group this ACL applies to. Blank if using userid. */
|
||||
string group;
|
||||
/** Binary mask of privileges to allow. */
|
||||
int allow;
|
||||
/** Binary mask of privileges to deny. */
|
||||
int deny;
|
||||
};
|
||||
|
||||
/** A single ip mask for a ban.
|
||||
**/
|
||||
struct Ban {
|
||||
/** Address to ban. */
|
||||
NetAddress address;
|
||||
/** Number of bits in ban to apply. */
|
||||
int bits;
|
||||
/** Username associated with ban. */
|
||||
string name;
|
||||
/** Hash of banned user. */
|
||||
string hash;
|
||||
/** Reason for ban. */
|
||||
string reason;
|
||||
/** Date ban was applied in unix time format. */
|
||||
int start;
|
||||
/** Duration of ban. */
|
||||
int duration;
|
||||
};
|
||||
|
||||
/** A entry in the log.
|
||||
**/
|
||||
struct LogEntry {
|
||||
/** Timestamp in UNIX time_t */
|
||||
int timestamp;
|
||||
/** The log message. */
|
||||
string txt;
|
||||
};
|
||||
|
||||
class Tree;
|
||||
sequence<Tree> TreeList;
|
||||
|
||||
enum ChannelInfo { ChannelDescription, ChannelPosition };
|
||||
enum UserInfo { UserName, UserEmail, UserComment, UserHash, UserPassword, UserLastActive, UserKDFIterations };
|
||||
|
||||
dictionary<int, User> UserMap;
|
||||
dictionary<int, Channel> ChannelMap;
|
||||
sequence<Channel> ChannelList;
|
||||
sequence<User> UserList;
|
||||
sequence<Group> GroupList;
|
||||
sequence<ACL> ACLList;
|
||||
sequence<LogEntry> LogList;
|
||||
sequence<Ban> BanList;
|
||||
sequence<int> IdList;
|
||||
sequence<string> NameList;
|
||||
dictionary<int, string> NameMap;
|
||||
dictionary<string, int> IdMap;
|
||||
sequence<byte> Texture;
|
||||
dictionary<string, string> ConfigMap;
|
||||
sequence<string> GroupNameList;
|
||||
sequence<byte> CertificateDer;
|
||||
sequence<CertificateDer> CertificateList;
|
||||
|
||||
/** User information map.
|
||||
* Older versions of ice-php can't handle enums as keys. If you are using one of these, replace 'UserInfo' with 'byte'.
|
||||
*/
|
||||
|
||||
dictionary<UserInfo, string> UserInfoMap;
|
||||
|
||||
/** User and subchannel state. Read-only.
|
||||
**/
|
||||
class Tree {
|
||||
/** Channel definition of current channel. */
|
||||
Channel c;
|
||||
/** List of subchannels. */
|
||||
TreeList children;
|
||||
/** Users in this channel. */
|
||||
UserList users;
|
||||
};
|
||||
|
||||
exception MurmurException {};
|
||||
/** This is thrown when you specify an invalid session. This may happen if the user has disconnected since your last call to {@link Server.getUsers}. See {@link User.session} */
|
||||
exception InvalidSessionException extends MurmurException {};
|
||||
/** This is thrown when you specify an invalid channel id. This may happen if the channel was removed by another provess. It can also be thrown if you try to add an invalid channel. */
|
||||
exception InvalidChannelException extends MurmurException {};
|
||||
/** This is thrown when you try to do an operation on a server that does not exist. This may happen if someone has removed the server. */
|
||||
exception InvalidServerException extends MurmurException {};
|
||||
/** This happens if you try to fetch user or channel state on a stopped server, if you try to stop an already stopped server or start an already started server. */
|
||||
exception ServerBootedException extends MurmurException {};
|
||||
/** This is thrown if {@link Server.start} fails, and should generally be the cause for some concern. */
|
||||
exception ServerFailureException extends MurmurException {};
|
||||
/** This is thrown when you specify an invalid userid. */
|
||||
exception InvalidUserException extends MurmurException {};
|
||||
/** This is thrown when you try to set an invalid texture. */
|
||||
exception InvalidTextureException extends MurmurException {};
|
||||
/** This is thrown when you supply an invalid callback. */
|
||||
exception InvalidCallbackException extends MurmurException {};
|
||||
/** This is thrown when you supply the wrong secret in the calling context. */
|
||||
exception InvalidSecretException extends MurmurException {};
|
||||
/** This is thrown when the channel operation would exceed the channel nesting limit */
|
||||
exception NestingLimitException extends MurmurException {};
|
||||
/** This is thrown when you ask the server to disclose something that should be secret. */
|
||||
exception WriteOnlyException extends MurmurException {};
|
||||
/** This is thrown when invalid input data was specified. */
|
||||
exception InvalidInputDataException extends MurmurException {};
|
||||
|
||||
/** Callback interface for servers. You can supply an implementation of this to receive notification
|
||||
* messages from the server.
|
||||
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||
* Please note that all callbacks are done asynchronously; murmur does not wait for the callback to
|
||||
* complete before continuing processing.
|
||||
* Note that callbacks are removed when a server is stopped, so you should have a callback for
|
||||
* {@link MetaCallback.started} which calls {@link Server.addCallback}.
|
||||
* @see MetaCallback
|
||||
* @see Server.addCallback
|
||||
*/
|
||||
interface ServerCallback {
|
||||
/** Called when a user connects to the server.
|
||||
* @param state State of connected user.
|
||||
*/
|
||||
idempotent void userConnected(User state);
|
||||
/** Called when a user disconnects from the server. The user has already been removed, so you can no longer use methods like {@link Server.getState}
|
||||
* to retrieve the user's state.
|
||||
* @param state State of disconnected user.
|
||||
*/
|
||||
idempotent void userDisconnected(User state);
|
||||
/** Called when a user state changes. This is called if the user moves, is renamed, is muted, deafened etc.
|
||||
* @param state New state of user.
|
||||
*/
|
||||
idempotent void userStateChanged(User state);
|
||||
/** Called when user writes a text message
|
||||
* @param state the User sending the message
|
||||
* @param message the TextMessage the user has sent
|
||||
*/
|
||||
idempotent void userTextMessage(User state, TextMessage message);
|
||||
/** Called when a new channel is created.
|
||||
* @param state State of new channel.
|
||||
*/
|
||||
idempotent void channelCreated(Channel state);
|
||||
/** Called when a channel is removed. The channel has already been removed, you can no longer use methods like {@link Server.getChannelState}
|
||||
* @param state State of removed channel.
|
||||
*/
|
||||
idempotent void channelRemoved(Channel state);
|
||||
/** Called when a new channel state changes. This is called if the channel is moved, renamed or if new links are added.
|
||||
* @param state New state of channel.
|
||||
*/
|
||||
idempotent void channelStateChanged(Channel state);
|
||||
};
|
||||
|
||||
/** Context for actions in the Server menu. */
|
||||
const int ContextServer = 0x01;
|
||||
/** Context for actions in the Channel menu. */
|
||||
const int ContextChannel = 0x02;
|
||||
/** Context for actions in the User menu. */
|
||||
const int ContextUser = 0x04;
|
||||
|
||||
/** Callback interface for context actions. You need to supply one of these for {@link Server.addContext}.
|
||||
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||
* Please note that all callbacks are done asynchronously; murmur does not wait for the callback to
|
||||
* complete before continuing processing.
|
||||
*/
|
||||
interface ServerContextCallback {
|
||||
/** Called when a context action is performed.
|
||||
* @param action Action to be performed.
|
||||
* @param usr User which initiated the action.
|
||||
* @param session If nonzero, session of target user.
|
||||
* @param channelid If not -1, id of target channel.
|
||||
*/
|
||||
idempotent void contextAction(string action, User usr, int session, int channelid);
|
||||
};
|
||||
|
||||
/** Callback interface for server authentication. You need to supply one of these for {@link Server.setAuthenticator}.
|
||||
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||
* Please note that unlike {@link ServerCallback} and {@link ServerContextCallback}, these methods are called
|
||||
* synchronously. If the response lags, the entire murmur server will lag.
|
||||
* Also note that, as the method calls are synchronous, making a call to {@link Server} or {@link Meta} will
|
||||
* deadlock the server.
|
||||
*/
|
||||
interface ServerAuthenticator {
|
||||
/** Called to authenticate a user. If you do not know the username in question, always return -2 from this
|
||||
* method to fall through to normal database authentication.
|
||||
* Note that if authentication succeeds, murmur will create a record of the user in it's database, reserving
|
||||
* the username and id so it cannot be used for normal database authentication.
|
||||
* The data in the certificate (name, email addresses etc), as well as the list of signing certificates,
|
||||
* should only be trusted if certstrong is true.
|
||||
*
|
||||
* Internally, Murmur treats usernames as case-insensitive. It is recommended
|
||||
* that authenticators do the same. Murmur checks if a username is in use when
|
||||
* a user connects. If the connecting user is registered, the other username is
|
||||
* kicked. If the connecting user is not registered, the connecting user is not
|
||||
* allowed to join the server.
|
||||
*
|
||||
* @param name Username to authenticate.
|
||||
* @param pw Password to authenticate with.
|
||||
* @param certificates List of der encoded certificates the user connected with.
|
||||
* @param certhash Hash of user certificate, as used by murmur internally when matching.
|
||||
* @param certstrong True if certificate was valid and signed by a trusted CA.
|
||||
* @param newname Set this to change the username from the supplied one.
|
||||
* @param groups List of groups on the root channel that the user will be added to for the duration of the connection.
|
||||
* @return UserID of authenticated user, -1 for authentication failures, -2 for unknown user (fallthrough),
|
||||
* -3 for authentication failures where the data could (temporarily) not be verified.
|
||||
*/
|
||||
idempotent int authenticate(string name, string pw, CertificateList certificates, string certhash, bool certstrong, out string newname, out GroupNameList groups);
|
||||
|
||||
/** Fetch information about a user. This is used to retrieve information like email address, keyhash etc. If you
|
||||
* want murmur to take care of this information itself, simply return false to fall through.
|
||||
* @param id User id.
|
||||
* @param info Information about user. This needs to include at least "name".
|
||||
* @return true if information is present, false to fall through.
|
||||
*/
|
||||
idempotent bool getInfo(int id, out UserInfoMap info);
|
||||
|
||||
/** Map a name to a user id.
|
||||
* @param name Username to map.
|
||||
* @return User id or -2 for unknown name.
|
||||
*/
|
||||
idempotent int nameToId(string name);
|
||||
|
||||
/** Map a user id to a username.
|
||||
* @param id User id to map.
|
||||
* @return Name of user or empty string for unknown id.
|
||||
*/
|
||||
idempotent string idToName(int id);
|
||||
|
||||
/** Map a user to a custom Texture.
|
||||
* @param id User id to map.
|
||||
* @return User texture or an empty texture for unknown users or users without textures.
|
||||
*/
|
||||
idempotent Texture idToTexture(int id);
|
||||
};
|
||||
|
||||
/** Callback interface for server authentication and registration. This allows you to support both authentication
|
||||
* and account updating.
|
||||
* You do not need to implement this if all you want is authentication, you only need this if other scripts
|
||||
* connected to the same server calls e.g. {@link Server.setTexture}.
|
||||
* Almost all of these methods support fall through, meaning murmur should continue the operation against its
|
||||
* own database.
|
||||
*/
|
||||
interface ServerUpdatingAuthenticator extends ServerAuthenticator {
|
||||
/** Register a new user.
|
||||
* @param info Information about user to register.
|
||||
* @return User id of new user, -1 for registration failure, or -2 to fall through.
|
||||
*/
|
||||
int registerUser(UserInfoMap info);
|
||||
|
||||
/** Unregister a user.
|
||||
* @param id Userid to unregister.
|
||||
* @return 1 for successful unregistration, 0 for unsuccessful unregistration, -1 to fall through.
|
||||
*/
|
||||
int unregisterUser(int id);
|
||||
|
||||
/** Get a list of registered users matching filter.
|
||||
* @param filter Substring usernames must contain. If empty, return all registered users.
|
||||
* @return List of matching registered users.
|
||||
*/
|
||||
idempotent NameMap getRegisteredUsers(string filter);
|
||||
|
||||
/** Set additional information for user registration.
|
||||
* @param id Userid of registered user.
|
||||
* @param info Information to set about user. This should be merged with existing information.
|
||||
* @return 1 for successful update, 0 for unsuccessful update, -1 to fall through.
|
||||
*/
|
||||
idempotent int setInfo(int id, UserInfoMap info);
|
||||
|
||||
/** Set texture (now called avatar) of user registration.
|
||||
* @param id registrationId of registered user.
|
||||
* @param tex New texture.
|
||||
* @return 1 for successful update, 0 for unsuccessful update, -1 to fall through.
|
||||
*/
|
||||
idempotent int setTexture(int id, Texture tex);
|
||||
};
|
||||
|
||||
/** Per-server interface. This includes all methods for configuring and altering
|
||||
* the state of a single virtual server. You can retrieve a pointer to this interface
|
||||
* from one of the methods in {@link Meta}.
|
||||
**/
|
||||
["amd"] interface Server {
|
||||
/** Shows if the server currently running (accepting users).
|
||||
*
|
||||
* @return Run-state of server.
|
||||
*/
|
||||
idempotent bool isRunning() throws InvalidSecretException;
|
||||
|
||||
/** Start server. */
|
||||
void start() throws ServerBootedException, ServerFailureException, InvalidSecretException;
|
||||
|
||||
/** Stop server.
|
||||
* Note: Server will be restarted on Murmur restart unless explicitly disabled
|
||||
* with setConf("boot", false)
|
||||
*/
|
||||
void stop() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Delete server and all it's configuration. */
|
||||
void delete() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Fetch the server id.
|
||||
*
|
||||
* @return Unique server id.
|
||||
*/
|
||||
idempotent int id() throws InvalidSecretException;
|
||||
|
||||
/** Add a callback. The callback will receive notifications about changes to users and channels.
|
||||
*
|
||||
* @param cb Callback interface which will receive notifications.
|
||||
* @see removeCallback
|
||||
*/
|
||||
void addCallback(ServerCallback *cb) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Remove a callback.
|
||||
*
|
||||
* @param cb Callback interface to be removed.
|
||||
* @see addCallback
|
||||
*/
|
||||
void removeCallback(ServerCallback *cb) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Set external authenticator. If set, all authentications from clients are forwarded to this
|
||||
* proxy.
|
||||
*
|
||||
* @param auth Authenticator object to perform subsequent authentications.
|
||||
*/
|
||||
void setAuthenticator(ServerAuthenticator *auth) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Retrieve configuration item.
|
||||
* @param key Configuration key.
|
||||
* @return Configuration value. If this is empty, see {@link Meta.getDefaultConf}
|
||||
*/
|
||||
idempotent string getConf(string key) throws InvalidSecretException, WriteOnlyException;
|
||||
|
||||
/** Retrieve all configuration items.
|
||||
* @return All configured values. If a value isn't set here, the value from {@link Meta.getDefaultConf} is used.
|
||||
*/
|
||||
idempotent ConfigMap getAllConf() throws InvalidSecretException;
|
||||
|
||||
/** Set a configuration item.
|
||||
* @param key Configuration key.
|
||||
* @param value Configuration value.
|
||||
*/
|
||||
idempotent void setConf(string key, string value) throws InvalidSecretException;
|
||||
|
||||
/** Set superuser password. This is just a convenience for using {@link updateRegistration} on user id 0.
|
||||
* @param pw Password.
|
||||
*/
|
||||
idempotent void setSuperuserPassword(string pw) throws InvalidSecretException;
|
||||
|
||||
/** Fetch log entries.
|
||||
* @param first Lowest numbered entry to fetch. 0 is the most recent item.
|
||||
* @param last Last entry to fetch.
|
||||
* @return List of log entries.
|
||||
*/
|
||||
idempotent LogList getLog(int first, int last) throws InvalidSecretException;
|
||||
|
||||
/** Fetch length of log
|
||||
* @return Number of entries in log
|
||||
*/
|
||||
idempotent int getLogLen() throws InvalidSecretException;
|
||||
|
||||
/** Fetch all users. This returns all currently connected users on the server.
|
||||
* @return List of connected users.
|
||||
* @see getState
|
||||
*/
|
||||
idempotent UserMap getUsers() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Fetch all channels. This returns all defined channels on the server. The root channel is always channel 0.
|
||||
* @return List of defined channels.
|
||||
* @see getChannelState
|
||||
*/
|
||||
idempotent ChannelMap getChannels() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Fetch certificate of user. This returns the complete certificate chain of a user.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @return Certificate list of user.
|
||||
*/
|
||||
idempotent CertificateList getCertificateList(int session) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Fetch all channels and connected users as a tree. This retrieves an easy-to-use representation of the server
|
||||
* as a tree. This is primarily used for viewing the state of the server on a webpage.
|
||||
* @return Recursive tree of all channels and connected users.
|
||||
*/
|
||||
idempotent Tree getTree() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Fetch all current IP bans on the server.
|
||||
* @return List of bans.
|
||||
*/
|
||||
idempotent BanList getBans() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Set all current IP bans on the server. This will replace any bans already present, so if you want to add a ban, be sure to call {@link getBans} and then
|
||||
* append to the returned list before calling this method.
|
||||
* @param bans List of bans.
|
||||
*/
|
||||
idempotent void setBans(BanList bans) throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Kick a user. The user is not banned, and is free to rejoin the server.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param reason Text message to show when user is kicked.
|
||||
*/
|
||||
void kickUser(int session, string reason) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Get state of a single connected user.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @return State of connected user.
|
||||
* @see setState
|
||||
* @see getUsers
|
||||
*/
|
||||
idempotent User getState(int session) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Set user state. You can use this to move, mute and deafen users.
|
||||
* @param state User state to set.
|
||||
* @see getState
|
||||
*/
|
||||
idempotent void setState(User state) throws ServerBootedException, InvalidSessionException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Send text message to a single user.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param text Message to send.
|
||||
* @see sendMessageChannel
|
||||
*/
|
||||
void sendMessage(int session, string text) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Check if user is permitted to perform action.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||
* @param perm Permission bits to check.
|
||||
* @return true if any of the permissions in perm were set for the user.
|
||||
*/
|
||||
bool hasPermission(int session, int channelid, int perm) throws ServerBootedException, InvalidSessionException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Return users effective permissions
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||
* @return bitfield of allowed actions
|
||||
*/
|
||||
idempotent int effectivePermissions(int session, int channelid) throws ServerBootedException, InvalidSessionException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Add a context callback. This is done per user, and will add a context menu action for the user.
|
||||
*
|
||||
* @param session Session of user which should receive context entry.
|
||||
* @param action Action string, a unique name to associate with the action.
|
||||
* @param text Name of action shown to user.
|
||||
* @param cb Callback interface which will receive notifications.
|
||||
* @param ctx Context this should be used in. Needs to be one or a combination of {@link ContextServer}, {@link ContextChannel} and {@link ContextUser}.
|
||||
* @see removeContextCallback
|
||||
*/
|
||||
void addContextCallback(int session, string action, string text, ServerContextCallback *cb, int ctx) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Remove a callback.
|
||||
*
|
||||
* @param cb Callback interface to be removed. This callback will be removed from all from all users.
|
||||
* @see addContextCallback
|
||||
*/
|
||||
void removeContextCallback(ServerContextCallback *cb) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Get state of single channel.
|
||||
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||
* @return State of channel.
|
||||
* @see setChannelState
|
||||
* @see getChannels
|
||||
*/
|
||||
idempotent Channel getChannelState(int channelid) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Set state of a single channel. You can use this to move or relink channels.
|
||||
* @param state Channel state to set.
|
||||
* @see getChannelState
|
||||
*/
|
||||
idempotent void setChannelState(Channel state) throws ServerBootedException, InvalidChannelException, InvalidSecretException, NestingLimitException;
|
||||
|
||||
/** Remove a channel and all its subchannels.
|
||||
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||
*/
|
||||
void removeChannel(int channelid) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Add a new channel.
|
||||
* @param name Name of new channel.
|
||||
* @param parent Channel ID of parent channel. See {@link Channel.id}.
|
||||
* @return ID of newly created channel.
|
||||
*/
|
||||
int addChannel(string name, int parent) throws ServerBootedException, InvalidChannelException, InvalidSecretException, NestingLimitException;
|
||||
|
||||
/** Send text message to channel or a tree of channels.
|
||||
* @param channelid Channel ID of channel to send to. See {@link Channel.id}.
|
||||
* @param tree If true, the message will be sent to the channel and all its subchannels.
|
||||
* @param text Message to send.
|
||||
* @see sendMessage
|
||||
*/
|
||||
void sendMessageChannel(int channelid, bool tree, string text) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Retrieve ACLs and Groups on a channel.
|
||||
* @param channelid Channel ID of channel to fetch from. See {@link Channel.id}.
|
||||
* @param acls List of ACLs on the channel. This will include inherited ACLs.
|
||||
* @param groups List of groups on the channel. This will include inherited groups.
|
||||
* @param inherit Does this channel inherit ACLs from the parent channel?
|
||||
*/
|
||||
idempotent void getACL(int channelid, out ACLList acls, out GroupList groups, out bool inherit) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Set ACLs and Groups on a channel. Note that this will replace all existing ACLs and groups on the channel.
|
||||
* @param channelid Channel ID of channel to fetch from. See {@link Channel.id}.
|
||||
* @param acls List of ACLs on the channel.
|
||||
* @param groups List of groups on the channel.
|
||||
* @param inherit Should this channel inherit ACLs from the parent channel?
|
||||
*/
|
||||
idempotent void setACL(int channelid, ACLList acls, GroupList groups, bool inherit) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Temporarily add a user to a group on a channel. This state is not saved, and is intended for temporary memberships.
|
||||
* @param channelid Channel ID of channel to add to. See {@link Channel.id}.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param group Group name to add to.
|
||||
*/
|
||||
idempotent void addUserToGroup(int channelid, int session, string group) throws ServerBootedException, InvalidChannelException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Remove a user from a temporary group membership on a channel. This state is not saved, and is intended for temporary memberships.
|
||||
* @param channelid Channel ID of channel to add to. See {@link Channel.id}.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param group Group name to remove from.
|
||||
*/
|
||||
idempotent void removeUserFromGroup(int channelid, int session, string group) throws ServerBootedException, InvalidChannelException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Redirect whisper targets for user. If set, whenever a user tries to whisper to group "source", the whisper will be redirected to group "target".
|
||||
* To remove a redirect pass an empty target string. This is intended for context groups.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param source Group name to redirect from.
|
||||
* @param target Group name to redirect to.
|
||||
*/
|
||||
idempotent void redirectWhisperGroup(int session, string source, string target) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Map a list of {@link User.userid} to a matching name.
|
||||
* @param List of ids.
|
||||
* @return Matching list of names, with an empty string representing invalid or unknown ids.
|
||||
*/
|
||||
idempotent NameMap getUserNames(IdList ids) throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Map a list of user names to a matching id.
|
||||
* @param List of names.
|
||||
* @reuturn List of matching ids, with -1 representing invalid or unknown user names.
|
||||
*/
|
||||
idempotent IdMap getUserIds(NameList names) throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Register a new user.
|
||||
* @param info Information about new user. Must include at least "name".
|
||||
* @return The ID of the user. See {@link RegisteredUser.userid}.
|
||||
*/
|
||||
int registerUser(UserInfoMap info) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||
|
||||
/** Remove a user registration.
|
||||
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||
*/
|
||||
void unregisterUser(int userid) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||
|
||||
/** Update the registration for a user. You can use this to set the email or password of a user,
|
||||
* and can also use it to change the user's name.
|
||||
* @param registration Updated registration record.
|
||||
*/
|
||||
idempotent void updateRegistration(int userid, UserInfoMap info) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||
|
||||
/** Fetch registration for a single user.
|
||||
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||
* @return Registration record.
|
||||
*/
|
||||
idempotent UserInfoMap getRegistration(int userid) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||
|
||||
/** Fetch a group of registered users.
|
||||
* @param filter Substring of user name. If blank, will retrieve all registered users.
|
||||
* @return List of registration records.
|
||||
*/
|
||||
idempotent NameMap getRegisteredUsers(string filter) throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Verify the password of a user. You can use this to verify a user's credentials.
|
||||
* @param name User name. See {@link RegisteredUser.name}.
|
||||
* @param pw User password.
|
||||
* @return User ID of registered user (See {@link RegisteredUser.userid}), -1 for failed authentication or -2 for unknown usernames.
|
||||
*/
|
||||
idempotent int verifyPassword(string name, string pw) throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Fetch user texture. Textures are stored as zlib compress()ed 600x60 32-bit BGRA data.
|
||||
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||
* @return Custom texture associated with user or an empty texture.
|
||||
*/
|
||||
idempotent Texture getTexture(int userid) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||
|
||||
/** Set a user texture (now called avatar).
|
||||
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||
* @param tex Texture (as a Byte-Array) to set for the user, or an empty texture to remove the existing texture.
|
||||
*/
|
||||
idempotent void setTexture(int userid, Texture tex) throws ServerBootedException, InvalidUserException, InvalidTextureException, InvalidSecretException;
|
||||
|
||||
/** Get virtual server uptime.
|
||||
* @return Uptime of the virtual server in seconds
|
||||
*/
|
||||
idempotent int getUptime() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/**
|
||||
* Update the server's certificate information.
|
||||
*
|
||||
* Reconfigure the running server's TLS socket with the given
|
||||
* certificate and private key.
|
||||
*
|
||||
* The certificate and and private key must be PEM formatted.
|
||||
*
|
||||
* New clients will see the new certificate.
|
||||
* Existing clients will continue to see the certificate the server
|
||||
* was using when they connected to it.
|
||||
*
|
||||
* This method throws InvalidInputDataException if any of the
|
||||
* following errors happen:
|
||||
* - Unable to decode the PEM certificate and/or private key.
|
||||
* - Unable to decrypt the private key with the given passphrase.
|
||||
* - The certificate and/or private key do not contain RSA keys.
|
||||
* - The certificate is not usable with the given private key.
|
||||
*/
|
||||
idempotent void updateCertificate(string certificate, string privateKey, string passphrase) throws ServerBootedException, InvalidSecretException, InvalidInputDataException;
|
||||
|
||||
/**
|
||||
* Makes the given user start listening to the given channel.
|
||||
* @param userid The ID of the user
|
||||
* @param channelid The ID of the channel
|
||||
*/
|
||||
idempotent void startListening(int userid, int channelid);
|
||||
|
||||
/**
|
||||
* Makes the given user stop listening to the given channel.
|
||||
* @param userid The ID of the user
|
||||
* @param channelid The ID of the channel
|
||||
*/
|
||||
idempotent void stopListening(int userid, int channelid);
|
||||
|
||||
/**
|
||||
* @param userid The ID of the user
|
||||
* @param channelid The ID of the channel
|
||||
* @returns Whether the given user is currently listening to the given channel
|
||||
*/
|
||||
idempotent bool isListening(int userid, int channelid);
|
||||
|
||||
/**
|
||||
* @param userid The ID of the user
|
||||
* @returns An ID-list of channels the given user is listening to
|
||||
*/
|
||||
idempotent IntList getListeningChannels(int userid);
|
||||
|
||||
/**
|
||||
* @param channelid The ID of the channel
|
||||
* @returns An ID-list of users listening to the given channel
|
||||
*/
|
||||
idempotent IntList getListeningUsers(int channelid);
|
||||
|
||||
/**
|
||||
* @param channelid The ID of the channel
|
||||
* @param userid The ID of the user
|
||||
* @returns The volume adjustment set for a listener of the given user in the given channel
|
||||
*/
|
||||
idempotent float getListenerVolumeAdjustment(int channelid, int userid);
|
||||
|
||||
/**
|
||||
* Sets the volume adjustment set for a listener of the given user in the given channel
|
||||
* @param channelid The ID of the channel
|
||||
* @param userid The ID of the user
|
||||
*/
|
||||
idempotent void setListenerVolumeAdjustment(int channelid, int userid, float volumeAdjustment);
|
||||
|
||||
/**
|
||||
* @param receiverUserIDs list of IDs of the users the message shall be sent to
|
||||
*/
|
||||
idempotent void sendWelcomeMessage(IdList receiverUserIDs);
|
||||
};
|
||||
|
||||
/** Callback interface for Meta. You can supply an implementation of this to receive notifications
|
||||
* when servers are stopped or started.
|
||||
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||
* Please note that all callbacks are done asynchronously; murmur does not wait for the callback to
|
||||
* complete before continuing processing.
|
||||
* @see ServerCallback
|
||||
* @see Meta.addCallback
|
||||
*/
|
||||
interface MetaCallback {
|
||||
/** Called when a server is started. The server is up and running when this event is sent, so all methods that
|
||||
* need a running server will work.
|
||||
* @param srv Interface for started server.
|
||||
*/
|
||||
void started(Server *srv);
|
||||
|
||||
/** Called when a server is stopped. The server is already stopped when this event is sent, so no methods that
|
||||
* need a running server will work.
|
||||
* @param srv Interface for started server.
|
||||
*/
|
||||
void stopped(Server *srv);
|
||||
};
|
||||
|
||||
sequence<Server *> ServerList;
|
||||
|
||||
/** This is the meta interface. It is primarily used for retrieving the {@link Server} interfaces for each individual server.
|
||||
**/
|
||||
["amd"] interface Meta {
|
||||
/** Fetch interface to specific server.
|
||||
* @param id Server ID. See {@link Server.getId}.
|
||||
* @return Interface for specified server, or a null proxy if id is invalid.
|
||||
*/
|
||||
idempotent Server *getServer(int id) throws InvalidSecretException;
|
||||
|
||||
/** Create a new server. Call {@link Server.getId} on the returned interface to find it's ID.
|
||||
* @return Interface for new server.
|
||||
*/
|
||||
Server *newServer() throws InvalidSecretException;
|
||||
|
||||
/** Fetch list of all currently running servers.
|
||||
* @return List of interfaces for running servers.
|
||||
*/
|
||||
idempotent ServerList getBootedServers() throws InvalidSecretException;
|
||||
|
||||
/** Fetch list of all defined servers.
|
||||
* @return List of interfaces for all servers.
|
||||
*/
|
||||
idempotent ServerList getAllServers() throws InvalidSecretException;
|
||||
|
||||
/** Fetch default configuration. This returns the configuration items that were set in the configuration file, or
|
||||
* the built-in default. The individual servers will use these values unless they have been overridden in the
|
||||
* server specific configuration. The only special case is the port, which defaults to the value defined here +
|
||||
* the servers ID - 1 (so that virtual server #1 uses the defined port, server #2 uses port+1 etc).
|
||||
* @return Default configuration of the servers.
|
||||
*/
|
||||
idempotent ConfigMap getDefaultConf() throws InvalidSecretException;
|
||||
|
||||
/** Fetch version of Murmur.
|
||||
* @param major Major version.
|
||||
* @param minor Minor version.
|
||||
* @param patch Patchlevel.
|
||||
* @param text Textual representation of version. Note that this may not match the {@link major}, {@link minor} and {@link patch} levels, as it
|
||||
* may be simply the compile date or the SVN revision. This is usually the text you want to present to users.
|
||||
*/
|
||||
idempotent void getVersion(out int major, out int minor, out int patch, out string text);
|
||||
|
||||
/** Add a callback. The callback will receive notifications when servers are started or stopped.
|
||||
*
|
||||
* @param cb Callback interface which will receive notifications.
|
||||
*/
|
||||
void addCallback(MetaCallback *cb) throws InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Remove a callback.
|
||||
*
|
||||
* @param cb Callback interface to be removed.
|
||||
*/
|
||||
void removeCallback(MetaCallback *cb) throws InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Get murmur uptime.
|
||||
* @return Uptime of murmur in seconds
|
||||
*/
|
||||
idempotent int getUptime();
|
||||
|
||||
/** Get slice file.
|
||||
* @return Contents of the slice file server compiled with.
|
||||
*/
|
||||
idempotent string getSlice();
|
||||
|
||||
/** Returns a checksum dict for the slice file.
|
||||
* @return Checksum dict
|
||||
*/
|
||||
idempotent Ice::SliceChecksumDict getSliceChecksums();
|
||||
};
|
||||
};
|
927
allianceauth/services/modules/mumble/Murmur.ice
Normal file
927
allianceauth/services/modules/mumble/Murmur.ice
Normal file
@ -0,0 +1,927 @@
|
||||
// Copyright 2008-2021 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
/**
|
||||
*
|
||||
* Information and control of the murmur server. Each server has
|
||||
* one {@link Meta} interface that controls global information, and
|
||||
* each virtual server has a {@link Server} interface.
|
||||
*
|
||||
**/
|
||||
|
||||
#include <Ice/SliceChecksumDict.ice>
|
||||
|
||||
module Murmur
|
||||
{
|
||||
|
||||
/** A network address in IPv6 format.
|
||||
**/
|
||||
["python:seq:tuple"] sequence<byte> NetAddress;
|
||||
|
||||
/** A connected user.
|
||||
**/
|
||||
struct User {
|
||||
/** Session ID. This identifies the connection to the server. */
|
||||
int session;
|
||||
/** User ID. -1 if the user is anonymous. */
|
||||
int userid;
|
||||
/** Is user muted by the server? */
|
||||
bool mute;
|
||||
/** Is user deafened by the server? If true, this implies mute. */
|
||||
bool deaf;
|
||||
/** Is the user suppressed by the server? This means the user is not muted, but does not have speech privileges in the current channel. */
|
||||
bool suppress;
|
||||
/** Is the user a priority speaker? */
|
||||
bool prioritySpeaker;
|
||||
/** Is the user self-muted? */
|
||||
bool selfMute;
|
||||
/** Is the user self-deafened? If true, this implies mute. */
|
||||
bool selfDeaf;
|
||||
/** Is the User recording? (This flag is read-only and cannot be changed using setState().) **/
|
||||
bool recording;
|
||||
/** Channel ID the user is in. Matches {@link Channel.id}. */
|
||||
int channel;
|
||||
/** The name of the user. */
|
||||
string name;
|
||||
/** Seconds user has been online. */
|
||||
int onlinesecs;
|
||||
/** Average transmission rate in bytes per second over the last few seconds. */
|
||||
int bytespersec;
|
||||
/** Legacy client version. */
|
||||
int version;
|
||||
/** New client version. (See https://github.com/mumble-voip/mumble/issues/5827) */
|
||||
long version2;
|
||||
/** Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else. */
|
||||
string release;
|
||||
/** Client OS. */
|
||||
string os;
|
||||
/** Client OS Version. */
|
||||
string osversion;
|
||||
/** Plugin Identity. This will be the user's unique ID inside the current game. */
|
||||
string identity;
|
||||
/**
|
||||
Base64-encoded Plugin context. This is a binary blob identifying the game and team the user is on.
|
||||
|
||||
The used Base64 alphabet is the one specified in RFC 2045.
|
||||
|
||||
Before Mumble 1.3.0, this string was not Base64-encoded. This could cause problems for some Ice
|
||||
implementations, such as the .NET implementation.
|
||||
|
||||
If you need the exact string that is used by Mumble, you can get it by Base64-decoding this string.
|
||||
|
||||
If you simply need to detect whether two users are in the same game world, string comparisons will
|
||||
continue to work as before.
|
||||
*/
|
||||
string context;
|
||||
/** User comment. Shown as tooltip for this user. */
|
||||
string comment;
|
||||
/** Client address. */
|
||||
NetAddress address;
|
||||
/** TCP only. True until UDP connectivity is established. */
|
||||
bool tcponly;
|
||||
/** Idle time. This is how many seconds it is since the user last spoke. Other activity is not counted. */
|
||||
int idlesecs;
|
||||
/** UDP Ping Average. This is the average ping for the user via UDP over the duration of the connection. */
|
||||
float udpPing;
|
||||
/** TCP Ping Average. This is the average ping for the user via TCP over the duration of the connection. */
|
||||
float tcpPing;
|
||||
};
|
||||
|
||||
sequence<int> IntList;
|
||||
|
||||
/** A text message between users.
|
||||
**/
|
||||
struct TextMessage {
|
||||
/** Sessions (connected users) who were sent this message. */
|
||||
IntList sessions;
|
||||
/** Channels who were sent this message. */
|
||||
IntList channels;
|
||||
/** Trees of channels who were sent this message. */
|
||||
IntList trees;
|
||||
/** The contents of the message. */
|
||||
string text;
|
||||
};
|
||||
|
||||
/** A channel.
|
||||
**/
|
||||
struct Channel {
|
||||
/** Channel ID. This is unique per channel, and the root channel is always id 0. */
|
||||
int id;
|
||||
/** Name of the channel. There can not be two channels with the same parent that has the same name. */
|
||||
string name;
|
||||
/** ID of parent channel, or -1 if this is the root channel. */
|
||||
int parent;
|
||||
/** List of id of linked channels. */
|
||||
IntList links;
|
||||
/** Description of channel. Shown as tooltip for this channel. */
|
||||
string description;
|
||||
/** Channel is temporary, and will be removed when the last user leaves it. */
|
||||
bool temporary;
|
||||
/** Position of the channel which is used in Client for sorting. */
|
||||
int position;
|
||||
};
|
||||
|
||||
/** A group. Groups are defined per channel, and can inherit members from parent channels.
|
||||
**/
|
||||
struct Group {
|
||||
/** Group name */
|
||||
string name;
|
||||
/** Is this group inherited from a parent channel? Read-only. */
|
||||
bool inherited;
|
||||
/** Does this group inherit members from parent channels? */
|
||||
bool inherit;
|
||||
/** Can subchannels inherit members from this group? */
|
||||
bool inheritable;
|
||||
/** List of users to add to the group. */
|
||||
IntList add;
|
||||
/** List of inherited users to remove from the group. */
|
||||
IntList remove;
|
||||
/** Current members of the group, including inherited members. Read-only. */
|
||||
IntList members;
|
||||
};
|
||||
|
||||
/** Write access to channel control. Implies all other permissions (except Speak). */
|
||||
const int PermissionWrite = 0x01;
|
||||
/** Traverse channel. Without this, a client cannot reach subchannels, no matter which privileges he has there. */
|
||||
const int PermissionTraverse = 0x02;
|
||||
/** Enter channel. */
|
||||
const int PermissionEnter = 0x04;
|
||||
/** Speak in channel. */
|
||||
const int PermissionSpeak = 0x08;
|
||||
/** Whisper to channel. This is different from Speak, so you can set up different permissions. */
|
||||
const int PermissionWhisper = 0x100;
|
||||
/** Mute and deafen other users in this channel. */
|
||||
const int PermissionMuteDeafen = 0x10;
|
||||
/** Move users from channel. You need this permission in both the source and destination channel to move another user. */
|
||||
const int PermissionMove = 0x20;
|
||||
/** Make new channel as a subchannel of this channel. */
|
||||
const int PermissionMakeChannel = 0x40;
|
||||
/** Make new temporary channel as a subchannel of this channel. */
|
||||
const int PermissionMakeTempChannel = 0x400;
|
||||
/** Link this channel. You need this permission in both the source and destination channel to link channels, or in either channel to unlink them. */
|
||||
const int PermissionLinkChannel = 0x80;
|
||||
/** Send text message to channel. */
|
||||
const int PermissionTextMessage = 0x200;
|
||||
/** Kick user from server. Only valid on root channel. */
|
||||
const int PermissionKick = 0x10000;
|
||||
/** Ban user from server. Only valid on root channel. */
|
||||
const int PermissionBan = 0x20000;
|
||||
/** Register and unregister users. Only valid on root channel. */
|
||||
const int PermissionRegister = 0x40000;
|
||||
/** Register and unregister users. Only valid on root channel. */
|
||||
const int PermissionRegisterSelf = 0x80000;
|
||||
/** Reset the comment or avatar of a user. Only valid on root channel. */
|
||||
const int ResetUserContent = 0x100000;
|
||||
|
||||
|
||||
/** Access Control List for a channel. ACLs are defined per channel, and can be inherited from parent channels.
|
||||
**/
|
||||
struct ACL {
|
||||
/** Does the ACL apply to this channel? */
|
||||
bool applyHere;
|
||||
/** Does the ACL apply to subchannels? */
|
||||
bool applySubs;
|
||||
/** Is this ACL inherited from a parent channel? Read-only. */
|
||||
bool inherited;
|
||||
/** ID of user this ACL applies to. -1 if using a group name. */
|
||||
int userid;
|
||||
/** Group this ACL applies to. Blank if using userid. */
|
||||
string group;
|
||||
/** Binary mask of privileges to allow. */
|
||||
int allow;
|
||||
/** Binary mask of privileges to deny. */
|
||||
int deny;
|
||||
};
|
||||
|
||||
/** A single ip mask for a ban.
|
||||
**/
|
||||
struct Ban {
|
||||
/** Address to ban. */
|
||||
NetAddress address;
|
||||
/** Number of bits in ban to apply. */
|
||||
int bits;
|
||||
/** Username associated with ban. */
|
||||
string name;
|
||||
/** Hash of banned user. */
|
||||
string hash;
|
||||
/** Reason for ban. */
|
||||
string reason;
|
||||
/** Date ban was applied in unix time format. */
|
||||
int start;
|
||||
/** Duration of ban. */
|
||||
int duration;
|
||||
};
|
||||
|
||||
/** A entry in the log.
|
||||
**/
|
||||
struct LogEntry {
|
||||
/** Timestamp in UNIX time_t */
|
||||
int timestamp;
|
||||
/** The log message. */
|
||||
string txt;
|
||||
};
|
||||
|
||||
class Tree;
|
||||
sequence<Tree> TreeList;
|
||||
|
||||
enum ChannelInfo { ChannelDescription, ChannelPosition };
|
||||
enum UserInfo { UserName, UserEmail, UserComment, UserHash, UserPassword, UserLastActive, UserKDFIterations };
|
||||
|
||||
dictionary<int, User> UserMap;
|
||||
dictionary<int, Channel> ChannelMap;
|
||||
sequence<Channel> ChannelList;
|
||||
sequence<User> UserList;
|
||||
sequence<Group> GroupList;
|
||||
sequence<ACL> ACLList;
|
||||
sequence<LogEntry> LogList;
|
||||
sequence<Ban> BanList;
|
||||
sequence<int> IdList;
|
||||
sequence<string> NameList;
|
||||
dictionary<int, string> NameMap;
|
||||
dictionary<string, int> IdMap;
|
||||
sequence<byte> Texture;
|
||||
dictionary<string, string> ConfigMap;
|
||||
sequence<string> GroupNameList;
|
||||
sequence<byte> CertificateDer;
|
||||
sequence<CertificateDer> CertificateList;
|
||||
|
||||
/** User information map.
|
||||
* Older versions of ice-php can't handle enums as keys. If you are using one of these, replace 'UserInfo' with 'byte'.
|
||||
*/
|
||||
|
||||
dictionary<UserInfo, string> UserInfoMap;
|
||||
|
||||
/** User and subchannel state. Read-only.
|
||||
**/
|
||||
class Tree {
|
||||
/** Channel definition of current channel. */
|
||||
Channel c;
|
||||
/** List of subchannels. */
|
||||
TreeList children;
|
||||
/** Users in this channel. */
|
||||
UserList users;
|
||||
};
|
||||
|
||||
exception MurmurException {};
|
||||
/** This is thrown when you specify an invalid session. This may happen if the user has disconnected since your last call to {@link Server.getUsers}. See {@link User.session} */
|
||||
exception InvalidSessionException extends MurmurException {};
|
||||
/** This is thrown when you specify an invalid channel id. This may happen if the channel was removed by another provess. It can also be thrown if you try to add an invalid channel. */
|
||||
exception InvalidChannelException extends MurmurException {};
|
||||
/** This is thrown when you try to do an operation on a server that does not exist. This may happen if someone has removed the server. */
|
||||
exception InvalidServerException extends MurmurException {};
|
||||
/** This happens if you try to fetch user or channel state on a stopped server, if you try to stop an already stopped server or start an already started server. */
|
||||
exception ServerBootedException extends MurmurException {};
|
||||
/** This is thrown if {@link Server.start} fails, and should generally be the cause for some concern. */
|
||||
exception ServerFailureException extends MurmurException {};
|
||||
/** This is thrown when you specify an invalid userid. */
|
||||
exception InvalidUserException extends MurmurException {};
|
||||
/** This is thrown when you try to set an invalid texture. */
|
||||
exception InvalidTextureException extends MurmurException {};
|
||||
/** This is thrown when you supply an invalid callback. */
|
||||
exception InvalidCallbackException extends MurmurException {};
|
||||
/** This is thrown when you supply the wrong secret in the calling context. */
|
||||
exception InvalidSecretException extends MurmurException {};
|
||||
/** This is thrown when the channel operation would exceed the channel nesting limit */
|
||||
exception NestingLimitException extends MurmurException {};
|
||||
/** This is thrown when you ask the server to disclose something that should be secret. */
|
||||
exception WriteOnlyException extends MurmurException {};
|
||||
/** This is thrown when invalid input data was specified. */
|
||||
exception InvalidInputDataException extends MurmurException {};
|
||||
|
||||
/** Callback interface for servers. You can supply an implementation of this to receive notification
|
||||
* messages from the server.
|
||||
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||
* Please note that all callbacks are done asynchronously; murmur does not wait for the callback to
|
||||
* complete before continuing processing.
|
||||
* Note that callbacks are removed when a server is stopped, so you should have a callback for
|
||||
* {@link MetaCallback.started} which calls {@link Server.addCallback}.
|
||||
* @see MetaCallback
|
||||
* @see Server.addCallback
|
||||
*/
|
||||
interface ServerCallback {
|
||||
/** Called when a user connects to the server.
|
||||
* @param state State of connected user.
|
||||
*/
|
||||
idempotent void userConnected(User state);
|
||||
/** Called when a user disconnects from the server. The user has already been removed, so you can no longer use methods like {@link Server.getState}
|
||||
* to retrieve the user's state.
|
||||
* @param state State of disconnected user.
|
||||
*/
|
||||
idempotent void userDisconnected(User state);
|
||||
/** Called when a user state changes. This is called if the user moves, is renamed, is muted, deafened etc.
|
||||
* @param state New state of user.
|
||||
*/
|
||||
idempotent void userStateChanged(User state);
|
||||
/** Called when user writes a text message
|
||||
* @param state the User sending the message
|
||||
* @param message the TextMessage the user has sent
|
||||
*/
|
||||
idempotent void userTextMessage(User state, TextMessage message);
|
||||
/** Called when a new channel is created.
|
||||
* @param state State of new channel.
|
||||
*/
|
||||
idempotent void channelCreated(Channel state);
|
||||
/** Called when a channel is removed. The channel has already been removed, you can no longer use methods like {@link Server.getChannelState}
|
||||
* @param state State of removed channel.
|
||||
*/
|
||||
idempotent void channelRemoved(Channel state);
|
||||
/** Called when a new channel state changes. This is called if the channel is moved, renamed or if new links are added.
|
||||
* @param state New state of channel.
|
||||
*/
|
||||
idempotent void channelStateChanged(Channel state);
|
||||
};
|
||||
|
||||
/** Context for actions in the Server menu. */
|
||||
const int ContextServer = 0x01;
|
||||
/** Context for actions in the Channel menu. */
|
||||
const int ContextChannel = 0x02;
|
||||
/** Context for actions in the User menu. */
|
||||
const int ContextUser = 0x04;
|
||||
|
||||
/** Callback interface for context actions. You need to supply one of these for {@link Server.addContext}.
|
||||
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||
* Please note that all callbacks are done asynchronously; murmur does not wait for the callback to
|
||||
* complete before continuing processing.
|
||||
*/
|
||||
interface ServerContextCallback {
|
||||
/** Called when a context action is performed.
|
||||
* @param action Action to be performed.
|
||||
* @param usr User which initiated the action.
|
||||
* @param session If nonzero, session of target user.
|
||||
* @param channelid If not -1, id of target channel.
|
||||
*/
|
||||
idempotent void contextAction(string action, User usr, int session, int channelid);
|
||||
};
|
||||
|
||||
/** Callback interface for server authentication. You need to supply one of these for {@link Server.setAuthenticator}.
|
||||
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||
* Please note that unlike {@link ServerCallback} and {@link ServerContextCallback}, these methods are called
|
||||
* synchronously. If the response lags, the entire murmur server will lag.
|
||||
* Also note that, as the method calls are synchronous, making a call to {@link Server} or {@link Meta} will
|
||||
* deadlock the server.
|
||||
*/
|
||||
interface ServerAuthenticator {
|
||||
/** Called to authenticate a user. If you do not know the username in question, always return -2 from this
|
||||
* method to fall through to normal database authentication.
|
||||
* Note that if authentication succeeds, murmur will create a record of the user in it's database, reserving
|
||||
* the username and id so it cannot be used for normal database authentication.
|
||||
* The data in the certificate (name, email addresses etc), as well as the list of signing certificates,
|
||||
* should only be trusted if certstrong is true.
|
||||
*
|
||||
* Internally, Murmur treats usernames as case-insensitive. It is recommended
|
||||
* that authenticators do the same. Murmur checks if a username is in use when
|
||||
* a user connects. If the connecting user is registered, the other username is
|
||||
* kicked. If the connecting user is not registered, the connecting user is not
|
||||
* allowed to join the server.
|
||||
*
|
||||
* @param name Username to authenticate.
|
||||
* @param pw Password to authenticate with.
|
||||
* @param certificates List of der encoded certificates the user connected with.
|
||||
* @param certhash Hash of user certificate, as used by murmur internally when matching.
|
||||
* @param certstrong True if certificate was valid and signed by a trusted CA.
|
||||
* @param newname Set this to change the username from the supplied one.
|
||||
* @param groups List of groups on the root channel that the user will be added to for the duration of the connection.
|
||||
* @return UserID of authenticated user, -1 for authentication failures, -2 for unknown user (fallthrough),
|
||||
* -3 for authentication failures where the data could (temporarily) not be verified.
|
||||
*/
|
||||
idempotent int authenticate(string name, string pw, CertificateList certificates, string certhash, bool certstrong, out string newname, out GroupNameList groups);
|
||||
|
||||
/** Fetch information about a user. This is used to retrieve information like email address, keyhash etc. If you
|
||||
* want murmur to take care of this information itself, simply return false to fall through.
|
||||
* @param id User id.
|
||||
* @param info Information about user. This needs to include at least "name".
|
||||
* @return true if information is present, false to fall through.
|
||||
*/
|
||||
idempotent bool getInfo(int id, out UserInfoMap info);
|
||||
|
||||
/** Map a name to a user id.
|
||||
* @param name Username to map.
|
||||
* @return User id or -2 for unknown name.
|
||||
*/
|
||||
idempotent int nameToId(string name);
|
||||
|
||||
/** Map a user id to a username.
|
||||
* @param id User id to map.
|
||||
* @return Name of user or empty string for unknown id.
|
||||
*/
|
||||
idempotent string idToName(int id);
|
||||
|
||||
/** Map a user to a custom Texture.
|
||||
* @param id User id to map.
|
||||
* @return User texture or an empty texture for unknown users or users without textures.
|
||||
*/
|
||||
idempotent Texture idToTexture(int id);
|
||||
};
|
||||
|
||||
/** Callback interface for server authentication and registration. This allows you to support both authentication
|
||||
* and account updating.
|
||||
* You do not need to implement this if all you want is authentication, you only need this if other scripts
|
||||
* connected to the same server calls e.g. {@link Server.setTexture}.
|
||||
* Almost all of these methods support fall through, meaning murmur should continue the operation against its
|
||||
* own database.
|
||||
*/
|
||||
interface ServerUpdatingAuthenticator extends ServerAuthenticator {
|
||||
/** Register a new user.
|
||||
* @param info Information about user to register.
|
||||
* @return User id of new user, -1 for registration failure, or -2 to fall through.
|
||||
*/
|
||||
int registerUser(UserInfoMap info);
|
||||
|
||||
/** Unregister a user.
|
||||
* @param id Userid to unregister.
|
||||
* @return 1 for successful unregistration, 0 for unsuccessful unregistration, -1 to fall through.
|
||||
*/
|
||||
int unregisterUser(int id);
|
||||
|
||||
/** Get a list of registered users matching filter.
|
||||
* @param filter Substring usernames must contain. If empty, return all registered users.
|
||||
* @return List of matching registered users.
|
||||
*/
|
||||
idempotent NameMap getRegisteredUsers(string filter);
|
||||
|
||||
/** Set additional information for user registration.
|
||||
* @param id Userid of registered user.
|
||||
* @param info Information to set about user. This should be merged with existing information.
|
||||
* @return 1 for successful update, 0 for unsuccessful update, -1 to fall through.
|
||||
*/
|
||||
idempotent int setInfo(int id, UserInfoMap info);
|
||||
|
||||
/** Set texture (now called avatar) of user registration.
|
||||
* @param id registrationId of registered user.
|
||||
* @param tex New texture.
|
||||
* @return 1 for successful update, 0 for unsuccessful update, -1 to fall through.
|
||||
*/
|
||||
idempotent int setTexture(int id, Texture tex);
|
||||
};
|
||||
|
||||
/** Per-server interface. This includes all methods for configuring and altering
|
||||
* the state of a single virtual server. You can retrieve a pointer to this interface
|
||||
* from one of the methods in {@link Meta}.
|
||||
**/
|
||||
["amd"] interface Server {
|
||||
/** Shows if the server currently running (accepting users).
|
||||
*
|
||||
* @return Run-state of server.
|
||||
*/
|
||||
idempotent bool isRunning() throws InvalidSecretException;
|
||||
|
||||
/** Start server. */
|
||||
void start() throws ServerBootedException, ServerFailureException, InvalidSecretException;
|
||||
|
||||
/** Stop server.
|
||||
* Note: Server will be restarted on Murmur restart unless explicitly disabled
|
||||
* with setConf("boot", false)
|
||||
*/
|
||||
void stop() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Delete server and all it's configuration. */
|
||||
void delete() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Fetch the server id.
|
||||
*
|
||||
* @return Unique server id.
|
||||
*/
|
||||
idempotent int id() throws InvalidSecretException;
|
||||
|
||||
/** Add a callback. The callback will receive notifications about changes to users and channels.
|
||||
*
|
||||
* @param cb Callback interface which will receive notifications.
|
||||
* @see removeCallback
|
||||
*/
|
||||
void addCallback(ServerCallback *cb) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Remove a callback.
|
||||
*
|
||||
* @param cb Callback interface to be removed.
|
||||
* @see addCallback
|
||||
*/
|
||||
void removeCallback(ServerCallback *cb) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Set external authenticator. If set, all authentications from clients are forwarded to this
|
||||
* proxy.
|
||||
*
|
||||
* @param auth Authenticator object to perform subsequent authentications.
|
||||
*/
|
||||
void setAuthenticator(ServerAuthenticator *auth) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Retrieve configuration item.
|
||||
* @param key Configuration key.
|
||||
* @return Configuration value. If this is empty, see {@link Meta.getDefaultConf}
|
||||
*/
|
||||
idempotent string getConf(string key) throws InvalidSecretException, WriteOnlyException;
|
||||
|
||||
/** Retrieve all configuration items.
|
||||
* @return All configured values. If a value isn't set here, the value from {@link Meta.getDefaultConf} is used.
|
||||
*/
|
||||
idempotent ConfigMap getAllConf() throws InvalidSecretException;
|
||||
|
||||
/** Set a configuration item.
|
||||
* @param key Configuration key.
|
||||
* @param value Configuration value.
|
||||
*/
|
||||
idempotent void setConf(string key, string value) throws InvalidSecretException;
|
||||
|
||||
/** Set superuser password. This is just a convenience for using {@link updateRegistration} on user id 0.
|
||||
* @param pw Password.
|
||||
*/
|
||||
idempotent void setSuperuserPassword(string pw) throws InvalidSecretException;
|
||||
|
||||
/** Fetch log entries.
|
||||
* @param first Lowest numbered entry to fetch. 0 is the most recent item.
|
||||
* @param last Last entry to fetch.
|
||||
* @return List of log entries.
|
||||
*/
|
||||
idempotent LogList getLog(int first, int last) throws InvalidSecretException;
|
||||
|
||||
/** Fetch length of log
|
||||
* @return Number of entries in log
|
||||
*/
|
||||
idempotent int getLogLen() throws InvalidSecretException;
|
||||
|
||||
/** Fetch all users. This returns all currently connected users on the server.
|
||||
* @return List of connected users.
|
||||
* @see getState
|
||||
*/
|
||||
idempotent UserMap getUsers() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Fetch all channels. This returns all defined channels on the server. The root channel is always channel 0.
|
||||
* @return List of defined channels.
|
||||
* @see getChannelState
|
||||
*/
|
||||
idempotent ChannelMap getChannels() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Fetch certificate of user. This returns the complete certificate chain of a user.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @return Certificate list of user.
|
||||
*/
|
||||
idempotent CertificateList getCertificateList(int session) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Fetch all channels and connected users as a tree. This retrieves an easy-to-use representation of the server
|
||||
* as a tree. This is primarily used for viewing the state of the server on a webpage.
|
||||
* @return Recursive tree of all channels and connected users.
|
||||
*/
|
||||
idempotent Tree getTree() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Fetch all current IP bans on the server.
|
||||
* @return List of bans.
|
||||
*/
|
||||
idempotent BanList getBans() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Set all current IP bans on the server. This will replace any bans already present, so if you want to add a ban, be sure to call {@link getBans} and then
|
||||
* append to the returned list before calling this method.
|
||||
* @param bans List of bans.
|
||||
*/
|
||||
idempotent void setBans(BanList bans) throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Kick a user. The user is not banned, and is free to rejoin the server.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param reason Text message to show when user is kicked.
|
||||
*/
|
||||
void kickUser(int session, string reason) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Get state of a single connected user.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @return State of connected user.
|
||||
* @see setState
|
||||
* @see getUsers
|
||||
*/
|
||||
idempotent User getState(int session) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Set user state. You can use this to move, mute and deafen users.
|
||||
* @param state User state to set.
|
||||
* @see getState
|
||||
*/
|
||||
idempotent void setState(User state) throws ServerBootedException, InvalidSessionException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Send text message to a single user.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param text Message to send.
|
||||
* @see sendMessageChannel
|
||||
*/
|
||||
void sendMessage(int session, string text) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Check if user is permitted to perform action.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||
* @param perm Permission bits to check.
|
||||
* @return true if any of the permissions in perm were set for the user.
|
||||
*/
|
||||
bool hasPermission(int session, int channelid, int perm) throws ServerBootedException, InvalidSessionException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Return users effective permissions
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||
* @return bitfield of allowed actions
|
||||
*/
|
||||
idempotent int effectivePermissions(int session, int channelid) throws ServerBootedException, InvalidSessionException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Add a context callback. This is done per user, and will add a context menu action for the user.
|
||||
*
|
||||
* @param session Session of user which should receive context entry.
|
||||
* @param action Action string, a unique name to associate with the action.
|
||||
* @param text Name of action shown to user.
|
||||
* @param cb Callback interface which will receive notifications.
|
||||
* @param ctx Context this should be used in. Needs to be one or a combination of {@link ContextServer}, {@link ContextChannel} and {@link ContextUser}.
|
||||
* @see removeContextCallback
|
||||
*/
|
||||
void addContextCallback(int session, string action, string text, ServerContextCallback *cb, int ctx) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Remove a callback.
|
||||
*
|
||||
* @param cb Callback interface to be removed. This callback will be removed from all from all users.
|
||||
* @see addContextCallback
|
||||
*/
|
||||
void removeContextCallback(ServerContextCallback *cb) throws ServerBootedException, InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Get state of single channel.
|
||||
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||
* @return State of channel.
|
||||
* @see setChannelState
|
||||
* @see getChannels
|
||||
*/
|
||||
idempotent Channel getChannelState(int channelid) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Set state of a single channel. You can use this to move or relink channels.
|
||||
* @param state Channel state to set.
|
||||
* @see getChannelState
|
||||
*/
|
||||
idempotent void setChannelState(Channel state) throws ServerBootedException, InvalidChannelException, InvalidSecretException, NestingLimitException;
|
||||
|
||||
/** Remove a channel and all its subchannels.
|
||||
* @param channelid ID of Channel. See {@link Channel.id}.
|
||||
*/
|
||||
void removeChannel(int channelid) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Add a new channel.
|
||||
* @param name Name of new channel.
|
||||
* @param parent Channel ID of parent channel. See {@link Channel.id}.
|
||||
* @return ID of newly created channel.
|
||||
*/
|
||||
int addChannel(string name, int parent) throws ServerBootedException, InvalidChannelException, InvalidSecretException, NestingLimitException;
|
||||
|
||||
/** Send text message to channel or a tree of channels.
|
||||
* @param channelid Channel ID of channel to send to. See {@link Channel.id}.
|
||||
* @param tree If true, the message will be sent to the channel and all its subchannels.
|
||||
* @param text Message to send.
|
||||
* @see sendMessage
|
||||
*/
|
||||
void sendMessageChannel(int channelid, bool tree, string text) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Retrieve ACLs and Groups on a channel.
|
||||
* @param channelid Channel ID of channel to fetch from. See {@link Channel.id}.
|
||||
* @param acls List of ACLs on the channel. This will include inherited ACLs.
|
||||
* @param groups List of groups on the channel. This will include inherited groups.
|
||||
* @param inherit Does this channel inherit ACLs from the parent channel?
|
||||
*/
|
||||
idempotent void getACL(int channelid, out ACLList acls, out GroupList groups, out bool inherit) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Set ACLs and Groups on a channel. Note that this will replace all existing ACLs and groups on the channel.
|
||||
* @param channelid Channel ID of channel to fetch from. See {@link Channel.id}.
|
||||
* @param acls List of ACLs on the channel.
|
||||
* @param groups List of groups on the channel.
|
||||
* @param inherit Should this channel inherit ACLs from the parent channel?
|
||||
*/
|
||||
idempotent void setACL(int channelid, ACLList acls, GroupList groups, bool inherit) throws ServerBootedException, InvalidChannelException, InvalidSecretException;
|
||||
|
||||
/** Temporarily add a user to a group on a channel. This state is not saved, and is intended for temporary memberships.
|
||||
* @param channelid Channel ID of channel to add to. See {@link Channel.id}.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param group Group name to add to.
|
||||
*/
|
||||
idempotent void addUserToGroup(int channelid, int session, string group) throws ServerBootedException, InvalidChannelException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Remove a user from a temporary group membership on a channel. This state is not saved, and is intended for temporary memberships.
|
||||
* @param channelid Channel ID of channel to add to. See {@link Channel.id}.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param group Group name to remove from.
|
||||
*/
|
||||
idempotent void removeUserFromGroup(int channelid, int session, string group) throws ServerBootedException, InvalidChannelException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Redirect whisper targets for user. If set, whenever a user tries to whisper to group "source", the whisper will be redirected to group "target".
|
||||
* To remove a redirect pass an empty target string. This is intended for context groups.
|
||||
* @param session Connection ID of user. See {@link User.session}.
|
||||
* @param source Group name to redirect from.
|
||||
* @param target Group name to redirect to.
|
||||
*/
|
||||
idempotent void redirectWhisperGroup(int session, string source, string target) throws ServerBootedException, InvalidSessionException, InvalidSecretException;
|
||||
|
||||
/** Map a list of {@link User.userid} to a matching name.
|
||||
* @param List of ids.
|
||||
* @return Matching list of names, with an empty string representing invalid or unknown ids.
|
||||
*/
|
||||
idempotent NameMap getUserNames(IdList ids) throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Map a list of user names to a matching id.
|
||||
* @param List of names.
|
||||
* @reuturn List of matching ids, with -1 representing invalid or unknown user names.
|
||||
*/
|
||||
idempotent IdMap getUserIds(NameList names) throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Register a new user.
|
||||
* @param info Information about new user. Must include at least "name".
|
||||
* @return The ID of the user. See {@link RegisteredUser.userid}.
|
||||
*/
|
||||
int registerUser(UserInfoMap info) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||
|
||||
/** Remove a user registration.
|
||||
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||
*/
|
||||
void unregisterUser(int userid) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||
|
||||
/** Update the registration for a user. You can use this to set the email or password of a user,
|
||||
* and can also use it to change the user's name.
|
||||
* @param registration Updated registration record.
|
||||
*/
|
||||
idempotent void updateRegistration(int userid, UserInfoMap info) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||
|
||||
/** Fetch registration for a single user.
|
||||
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||
* @return Registration record.
|
||||
*/
|
||||
idempotent UserInfoMap getRegistration(int userid) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||
|
||||
/** Fetch a group of registered users.
|
||||
* @param filter Substring of user name. If blank, will retrieve all registered users.
|
||||
* @return List of registration records.
|
||||
*/
|
||||
idempotent NameMap getRegisteredUsers(string filter) throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Verify the password of a user. You can use this to verify a user's credentials.
|
||||
* @param name User name. See {@link RegisteredUser.name}.
|
||||
* @param pw User password.
|
||||
* @return User ID of registered user (See {@link RegisteredUser.userid}), -1 for failed authentication or -2 for unknown usernames.
|
||||
*/
|
||||
idempotent int verifyPassword(string name, string pw) throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/** Fetch user texture. Textures are stored as zlib compress()ed 600x60 32-bit BGRA data.
|
||||
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||
* @return Custom texture associated with user or an empty texture.
|
||||
*/
|
||||
idempotent Texture getTexture(int userid) throws ServerBootedException, InvalidUserException, InvalidSecretException;
|
||||
|
||||
/** Set a user texture (now called avatar).
|
||||
* @param userid ID of registered user. See {@link RegisteredUser.userid}.
|
||||
* @param tex Texture (as a Byte-Array) to set for the user, or an empty texture to remove the existing texture.
|
||||
*/
|
||||
idempotent void setTexture(int userid, Texture tex) throws ServerBootedException, InvalidUserException, InvalidTextureException, InvalidSecretException;
|
||||
|
||||
/** Get virtual server uptime.
|
||||
* @return Uptime of the virtual server in seconds
|
||||
*/
|
||||
idempotent int getUptime() throws ServerBootedException, InvalidSecretException;
|
||||
|
||||
/**
|
||||
* Update the server's certificate information.
|
||||
*
|
||||
* Reconfigure the running server's TLS socket with the given
|
||||
* certificate and private key.
|
||||
*
|
||||
* The certificate and and private key must be PEM formatted.
|
||||
*
|
||||
* New clients will see the new certificate.
|
||||
* Existing clients will continue to see the certificate the server
|
||||
* was using when they connected to it.
|
||||
*
|
||||
* This method throws InvalidInputDataException if any of the
|
||||
* following errors happen:
|
||||
* - Unable to decode the PEM certificate and/or private key.
|
||||
* - Unable to decrypt the private key with the given passphrase.
|
||||
* - The certificate and/or private key do not contain RSA keys.
|
||||
* - The certificate is not usable with the given private key.
|
||||
*/
|
||||
idempotent void updateCertificate(string certificate, string privateKey, string passphrase) throws ServerBootedException, InvalidSecretException, InvalidInputDataException;
|
||||
|
||||
/**
|
||||
* Makes the given user start listening to the given channel.
|
||||
* @param userid The ID of the user
|
||||
* @param channelid The ID of the channel
|
||||
*/
|
||||
idempotent void startListening(int userid, int channelid);
|
||||
|
||||
/**
|
||||
* Makes the given user stop listening to the given channel.
|
||||
* @param userid The ID of the user
|
||||
* @param channelid The ID of the channel
|
||||
*/
|
||||
idempotent void stopListening(int userid, int channelid);
|
||||
|
||||
/**
|
||||
* @param userid The ID of the user
|
||||
* @param channelid The ID of the channel
|
||||
* @returns Whether the given user is currently listening to the given channel
|
||||
*/
|
||||
idempotent bool isListening(int userid, int channelid);
|
||||
|
||||
/**
|
||||
* @param userid The ID of the user
|
||||
* @returns An ID-list of channels the given user is listening to
|
||||
*/
|
||||
idempotent IntList getListeningChannels(int userid);
|
||||
|
||||
/**
|
||||
* @param channelid The ID of the channel
|
||||
* @returns An ID-list of users listening to the given channel
|
||||
*/
|
||||
idempotent IntList getListeningUsers(int channelid);
|
||||
|
||||
/**
|
||||
* @param receiverUserIDs list of IDs of the users the message shall be sent to
|
||||
*/
|
||||
idempotent void sendWelcomeMessage(IdList receiverUserIDs);
|
||||
};
|
||||
|
||||
/** Callback interface for Meta. You can supply an implementation of this to receive notifications
|
||||
* when servers are stopped or started.
|
||||
* If an added callback ever throws an exception or goes away, it will be automatically removed.
|
||||
* Please note that all callbacks are done asynchronously; murmur does not wait for the callback to
|
||||
* complete before continuing processing.
|
||||
* @see ServerCallback
|
||||
* @see Meta.addCallback
|
||||
*/
|
||||
interface MetaCallback {
|
||||
/** Called when a server is started. The server is up and running when this event is sent, so all methods that
|
||||
* need a running server will work.
|
||||
* @param srv Interface for started server.
|
||||
*/
|
||||
void started(Server *srv);
|
||||
|
||||
/** Called when a server is stopped. The server is already stopped when this event is sent, so no methods that
|
||||
* need a running server will work.
|
||||
* @param srv Interface for started server.
|
||||
*/
|
||||
void stopped(Server *srv);
|
||||
};
|
||||
|
||||
sequence<Server *> ServerList;
|
||||
|
||||
/** This is the meta interface. It is primarily used for retrieving the {@link Server} interfaces for each individual server.
|
||||
**/
|
||||
["amd"] interface Meta {
|
||||
/** Fetch interface to specific server.
|
||||
* @param id Server ID. See {@link Server.getId}.
|
||||
* @return Interface for specified server, or a null proxy if id is invalid.
|
||||
*/
|
||||
idempotent Server *getServer(int id) throws InvalidSecretException;
|
||||
|
||||
/** Create a new server. Call {@link Server.getId} on the returned interface to find it's ID.
|
||||
* @return Interface for new server.
|
||||
*/
|
||||
Server *newServer() throws InvalidSecretException;
|
||||
|
||||
/** Fetch list of all currently running servers.
|
||||
* @return List of interfaces for running servers.
|
||||
*/
|
||||
idempotent ServerList getBootedServers() throws InvalidSecretException;
|
||||
|
||||
/** Fetch list of all defined servers.
|
||||
* @return List of interfaces for all servers.
|
||||
*/
|
||||
idempotent ServerList getAllServers() throws InvalidSecretException;
|
||||
|
||||
/** Fetch default configuration. This returns the configuration items that were set in the configuration file, or
|
||||
* the built-in default. The individual servers will use these values unless they have been overridden in the
|
||||
* server specific configuration. The only special case is the port, which defaults to the value defined here +
|
||||
* the servers ID - 1 (so that virtual server #1 uses the defined port, server #2 uses port+1 etc).
|
||||
* @return Default configuration of the servers.
|
||||
*/
|
||||
idempotent ConfigMap getDefaultConf() throws InvalidSecretException;
|
||||
|
||||
/** Fetch version of Murmur.
|
||||
* @param major Major version.
|
||||
* @param minor Minor version.
|
||||
* @param patch Patchlevel.
|
||||
* @param text Textual representation of version. Note that this may not match the {@link major}, {@link minor} and {@link patch} levels, as it
|
||||
* may be simply the compile date or the SVN revision. This is usually the text you want to present to users.
|
||||
*/
|
||||
idempotent void getVersion(out int major, out int minor, out int patch, out string text);
|
||||
|
||||
/** Add a callback. The callback will receive notifications when servers are started or stopped.
|
||||
*
|
||||
* @param cb Callback interface which will receive notifications.
|
||||
*/
|
||||
void addCallback(MetaCallback *cb) throws InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Remove a callback.
|
||||
*
|
||||
* @param cb Callback interface to be removed.
|
||||
*/
|
||||
void removeCallback(MetaCallback *cb) throws InvalidCallbackException, InvalidSecretException;
|
||||
|
||||
/** Get murmur uptime.
|
||||
* @return Uptime of murmur in seconds
|
||||
*/
|
||||
idempotent int getUptime();
|
||||
|
||||
/** Get slice file.
|
||||
* @return Contents of the slice file server compiled with.
|
||||
*/
|
||||
idempotent string getSlice();
|
||||
|
||||
/** Returns a checksum dict for the slice file.
|
||||
* @return Checksum dict
|
||||
*/
|
||||
idempotent Ice::SliceChecksumDict getSliceChecksums();
|
||||
};
|
||||
};
|
@ -1,18 +1,29 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import IdlerHandler, MumbleServerServer, MumbleUser
|
||||
from ...admin import ServicesUserAdmin
|
||||
from .models import MumbleUser
|
||||
|
||||
|
||||
@admin.register(MumbleUser)
|
||||
class MumbleUserAdmin(ServicesUserAdmin):
|
||||
list_display = ServicesUserAdmin.list_display + (
|
||||
'username',
|
||||
'groups',
|
||||
'release',
|
||||
'version'
|
||||
)
|
||||
search_fields = ServicesUserAdmin.search_fields + (
|
||||
'username',
|
||||
'groups'
|
||||
)
|
||||
|
||||
fields = ('user', 'username', 'groups') # pwhash is hidden from admin panel
|
||||
|
||||
@admin.register(MumbleServerServer)
|
||||
class MumbleServerServerAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "ip", "port", "slice", "virtual_servers", "avatar_enable", "reject_on_error", "offset"]
|
||||
list_filter = ["slice", "avatar_enable", "reject_on_error", "offset"]
|
||||
search_fields = ["name"]
|
||||
|
||||
|
||||
@admin.register(IdlerHandler)
|
||||
class IdlerhandlerAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "enabled", "seconds", "interval", "channel", "denylist"]
|
||||
list_filter = ["enabled", "denylist"]
|
||||
search_fields = ["name"]
|
||||
|
5
allianceauth/services/modules/mumble/app_settings.py
Normal file
5
allianceauth/services/modules/mumble/app_settings.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.conf import settings
|
||||
|
||||
MUMBLE_TEMPS_FORCE_SSO = getattr(settings, "MUMBLE_TEMPS_FORCE_SSO", True)
|
||||
MUMBLE_TEMPS_SSO_PREFIX = getattr(settings, "MUMBLE_TEMPS_SSO_PREFIX", "[TEMP]")
|
||||
MUMBLE_TEMPS_LOGIN_PREFIX = getattr(settings, "MUMBLE_TEMPS_LOGIN_PREFIX", "[*TEMP]")
|
@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
from allianceauth.menu.hooks import MenuItemHook
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
@ -11,6 +12,7 @@ from allianceauth.services.hooks import ServicesHook
|
||||
from .models import MumbleUser
|
||||
from .tasks import MumbleTasks
|
||||
from .urls import urlpatterns
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -36,16 +38,6 @@ class MumbleService(ServicesHook):
|
||||
except MumbleUser.DoesNotExist:
|
||||
logging.debug("User does not have a mumble account")
|
||||
|
||||
def update_groups(self, user):
|
||||
logger.debug(f"Updating {self.name} groups for {user}")
|
||||
if MumbleTasks.has_account(user):
|
||||
MumbleTasks.update_groups.delay(user.pk)
|
||||
|
||||
def sync_nickname(self, user):
|
||||
logger.debug(f"Updating {self.name} nickname for {user}")
|
||||
if MumbleTasks.has_account(user):
|
||||
MumbleTasks.update_display_name.apply_async(args=[user.pk], countdown=5) # cooldown on this task to ensure DB clean when syncing
|
||||
|
||||
def validate_user(self, user):
|
||||
if MumbleTasks.has_account(user) and not self.service_active_for_user(user):
|
||||
self.delete_user(user, notify_user=True)
|
||||
@ -74,5 +66,26 @@ class MumbleService(ServicesHook):
|
||||
|
||||
|
||||
@hooks.register('services_hook')
|
||||
def register_mumble_service():
|
||||
def register_mumble_service() -> ServicesHook:
|
||||
return MumbleService()
|
||||
|
||||
|
||||
class MumbleMenuItem(MenuItemHook):
|
||||
def __init__(self) -> None:
|
||||
MenuItemHook.__init__(
|
||||
self=self,
|
||||
text=_("Mumble Temp Links"),
|
||||
classes="fa-solid fa-microphone",
|
||||
url_name="mumble:templinks",
|
||||
navactive=["mumble:templinks"],
|
||||
)
|
||||
|
||||
def render(self, request) -> str:
|
||||
if request.user.has_perm("mumble.create_new_templinks"):
|
||||
return MenuItemHook.render(self, request)
|
||||
return ""
|
||||
|
||||
|
||||
@hooks.register("menu_item_hook")
|
||||
def register_menu() -> MumbleMenuItem:
|
||||
return MumbleMenuItem()
|
||||
|
631
allianceauth/services/modules/mumble/authenticator.py
Normal file
631
allianceauth/services/modules/mumble/authenticator.py
Normal file
@ -0,0 +1,631 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (C) 2010 Stefan Hacker <dd0t@users.sourceforge.net>
|
||||
# All rights reserved.
|
||||
# Adapted by Adarnof for AllianceAuth
|
||||
# Further modified by the Alliance Auth team and contributers
|
||||
# Rewritten for Django Context by the Alliance Auth Team
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
|
||||
# - Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# - Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# - Neither the name of the Mumble Developers nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
import importlib.util
|
||||
import logging
|
||||
import Ice
|
||||
|
||||
from urllib.request import urlopen
|
||||
from threading import Timer
|
||||
from passlib.hash import bcrypt_sha256
|
||||
from hashlib import sha1
|
||||
|
||||
import django
|
||||
from django.utils.datetime_safe import datetime
|
||||
import os
|
||||
import sys
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
sys.path.append(os.getcwd())
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myauth.settings.local")
|
||||
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
||||
django.setup()
|
||||
|
||||
from allianceauth import __version__ # noqa
|
||||
from allianceauth.services.modules.mumble.models import MumbleServerServer, MumbleUser, TempUser # noqa
|
||||
|
||||
|
||||
def main(server_id: int = 1) -> None:
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
server_id (int, optional): _description_. Defaults to 1.
|
||||
|
||||
Raises:
|
||||
e: _description_
|
||||
Murmur.InvalidSecretException: _description_
|
||||
e: _description_
|
||||
|
||||
Returns:
|
||||
_type_: _description_
|
||||
"""
|
||||
slicedir = Ice.getSliceDir()
|
||||
if not slicedir:
|
||||
slicedir = ["-I/usr/share/Ice/slice", "-I/usr/share/slice"]
|
||||
else:
|
||||
slicedir = ['-I' + slicedir]
|
||||
|
||||
package_dir = next(iter(importlib.util.find_spec('allianceauth.services.modules.mumble').submodule_search_locations))
|
||||
package_ice = os.path.join(package_dir, server_config_obj.slice)
|
||||
|
||||
logger.info(f"Using slice file: {package_ice}")
|
||||
slicedir.append("-I" + os.path.dirname(package_ice))
|
||||
|
||||
Ice.loadSlice("", slicedir + [package_ice])
|
||||
try:
|
||||
import MumbleServer as Murmur # Mumble >=1.5.17
|
||||
except ImportError:
|
||||
import Murmur # Mumble <=1.5.17
|
||||
|
||||
server_config_obj = MumbleServerServer.objects.get(id=server_id)
|
||||
|
||||
class AllianceAuthAuthenticatorApp(Ice.Application):
|
||||
def run(self, args) -> int:
|
||||
self.shutdownOnInterrupt()
|
||||
|
||||
if not self.initializeIceConnection():
|
||||
return 1
|
||||
|
||||
if server_config_obj.watchdog > 0:
|
||||
self.failedWatch = True
|
||||
self.checkConnection()
|
||||
|
||||
# Serve till we are stopped
|
||||
self.communicator().waitForShutdown()
|
||||
self.watchdog.cancel()
|
||||
|
||||
if self.interrupted():
|
||||
logger.warning("Caught interrupt, shutting down")
|
||||
|
||||
return 0
|
||||
|
||||
def initializeIceConnection(self) -> bool:
|
||||
"""
|
||||
Establishes the two-way Ice connection and adds the authenticator to the
|
||||
configured servers
|
||||
"""
|
||||
ice = self.communicator()
|
||||
logger.debug("Using shared ice secret")
|
||||
ice.getImplicitContext().put("secret", server_config_obj.secret)
|
||||
|
||||
logger.info(f"Connecting to Ice server ({server_config_obj.ip}:{server_config_obj.port})")
|
||||
base = ice.stringToProxy(f"Meta:tcp -h {server_config_obj.ip} -p {server_config_obj.port}")
|
||||
self.meta = Murmur.MetaPrx.uncheckedCast(base)
|
||||
|
||||
adapter = ice.createObjectAdapterWithEndpoints("Callback.Client", f"tcp -h {server_config_obj.endpoint}")
|
||||
adapter.activate()
|
||||
|
||||
metacbprx = adapter.addWithUUID(metaCallback(self))
|
||||
self.metacb = Murmur.MetaCallbackPrx.uncheckedCast(metacbprx)
|
||||
|
||||
servercbprx = adapter.addWithUUID(serverCallback(self))
|
||||
self.servercb = Murmur.ServerCallbackPrx.uncheckedCast(servercbprx)
|
||||
|
||||
authprx = adapter.addWithUUID(AllianceAuthAuthenticator())
|
||||
self.auth = Murmur.ServerUpdatingAuthenticatorPrx.uncheckedCast(authprx)
|
||||
|
||||
return self.attachCallbacks()
|
||||
|
||||
def attachCallbacks(self, quiet=False) -> bool:
|
||||
"""
|
||||
Attaches all callbacks for meta and authenticators
|
||||
"""
|
||||
|
||||
# Ice.ConnectionRefusedException
|
||||
# logger.debug('Attaching callbacks')
|
||||
try:
|
||||
if not quiet:
|
||||
logger.info("Attaching meta callback")
|
||||
|
||||
self.meta.addCallback(self.metacb)
|
||||
|
||||
for server in self.meta.getBootedServers():
|
||||
if server.id() in server_config_obj.virtual_servers_list():
|
||||
logger.info("Setting authenticator for virtual server %d", server.id())
|
||||
server.setAuthenticator(self.auth)
|
||||
server.addCallback(self.servercb)
|
||||
if server_config_obj.idler_handler:
|
||||
idler_handler(server)
|
||||
|
||||
except (Murmur.InvalidSecretException, Ice.UnknownUserException, Ice.ConnectionRefusedException) as e:
|
||||
logger.exception(e)
|
||||
if isinstance(e, Ice.ConnectionRefusedException):
|
||||
logger.error("Server refused connection")
|
||||
elif (isinstance(e, Murmur.InvalidSecretException) or isinstance(e, Ice.UnknownUserException) and (e.unknown == "Murmur::InvalidSecretException")):
|
||||
logger.error("Invalid ice secret")
|
||||
else:
|
||||
# We do not actually want to handle this one, re-raise it
|
||||
raise e
|
||||
|
||||
self.connected = False
|
||||
return False
|
||||
|
||||
self.connected = True
|
||||
return True
|
||||
|
||||
def checkConnection(self) -> None:
|
||||
"""
|
||||
Tries reapplies all callbacks to make sure the authenticator
|
||||
survives server restarts and disconnects.
|
||||
"""
|
||||
try:
|
||||
if not self.attachCallbacks(quiet=not self.failedWatch):
|
||||
self.failedWatch = True
|
||||
else:
|
||||
self.failedWatch = False
|
||||
except Ice.Exception as e:
|
||||
logger.error(f"Failed connection check, will retry in next watchdog run ({server_config_obj.watchdog}s)")
|
||||
logger.exception(e)
|
||||
self.failedWatch = True
|
||||
|
||||
# Renew the timer
|
||||
self.watchdog = Timer(server_config_obj.watchdog, self.checkConnection)
|
||||
self.watchdog.start()
|
||||
|
||||
def checkSecret(func):
|
||||
"""
|
||||
Decorator that checks whether the server transmitted the right secret
|
||||
if a secret is supposed to be used.
|
||||
"""
|
||||
if not server_config_obj.secret:
|
||||
return func
|
||||
|
||||
def newfunc(*args, **kws):
|
||||
if "current" in kws:
|
||||
current = kws["current"]
|
||||
else:
|
||||
current = args[-1]
|
||||
|
||||
if not current or "secret" not in current.ctx or current.ctx["secret"] != server_config_obj.secret:
|
||||
logger.error("Server transmitted invalid secret. Possible injection attempt.")
|
||||
raise Murmur.InvalidSecretException()
|
||||
|
||||
return func(*args, **kws)
|
||||
|
||||
return newfunc
|
||||
|
||||
def fortifyIceFu(retval=None, exceptions=(Ice.Exception,)):
|
||||
"""
|
||||
Decorator that catches exceptions,logs them and returns a safe retval
|
||||
value. This helps preventing the authenticator getting stuck in
|
||||
critical code paths. Only exceptions that are instances of classes
|
||||
given in the exceptions list are not caught.
|
||||
|
||||
The default is to catch all non-Ice exceptions.
|
||||
"""
|
||||
|
||||
def newdec(func):
|
||||
def newfunc(*args, **kws):
|
||||
try:
|
||||
return func(*args, **kws)
|
||||
except Exception as e:
|
||||
catch = True
|
||||
for ex in exceptions:
|
||||
if isinstance(e, ex):
|
||||
catch = False
|
||||
break
|
||||
if catch:
|
||||
logger.critical("Unexpected exception caught")
|
||||
logger.exception(e)
|
||||
return retval
|
||||
raise
|
||||
|
||||
return newfunc
|
||||
|
||||
return newdec
|
||||
|
||||
class metaCallback(Murmur.MetaCallback):
|
||||
def __init__(self, app) -> None:
|
||||
Murmur.MetaCallback.__init__(self)
|
||||
self.app = app
|
||||
|
||||
@fortifyIceFu()
|
||||
@checkSecret
|
||||
def started(self, server, current=None) -> None:
|
||||
"""
|
||||
This function is called when a virtual server is started
|
||||
and makes sure an authenticator gets attached if needed.
|
||||
"""
|
||||
if server.id() in server_config_obj.virtual_servers_list():
|
||||
logger.info("Setting authenticator for virtual server %d", server.id())
|
||||
try:
|
||||
server.setAuthenticator(self.app.auth)
|
||||
# Apparently this server was restarted without us noticing
|
||||
except (Murmur.InvalidSecretException, Ice.UnknownUserException) as e:
|
||||
if (hasattr(e, "unknown") and e.unknown != "Murmur::InvalidSecretException"):
|
||||
# Special handling for Murmur 1.2.2 servers with invalid slice files
|
||||
raise e
|
||||
logger.error("Invalid ice secret")
|
||||
return
|
||||
else:
|
||||
logger.debug("Virtual server %d got started", server.id())
|
||||
|
||||
@fortifyIceFu()
|
||||
@checkSecret
|
||||
def stopped(self, server, current=None) -> None:
|
||||
"""
|
||||
This function is called when a virtual server is stopped
|
||||
"""
|
||||
if self.app.connected:
|
||||
# Only try to output the server id if we think we are still connected to prevent
|
||||
# flooding of our thread pool
|
||||
try:
|
||||
if server.id() in server_config_obj.virtual_servers_list():
|
||||
logger.info(f"Authenticated virtual server {server.id()} got stopped")
|
||||
else:
|
||||
logger.debug(f"Virtual server {server.id()} got stopped")
|
||||
return
|
||||
except Ice.ConnectionRefusedException:
|
||||
self.app.connected = False
|
||||
|
||||
logger.debug("Server shutdown stopped a virtual server")
|
||||
|
||||
if server_config_obj.reject_on_error: # Python 2.4 compat
|
||||
authenticateFortifyResult = (-1, None, None)
|
||||
else:
|
||||
authenticateFortifyResult = (-2, None, None)
|
||||
|
||||
class serverCallback(Murmur.ServerCallback):
|
||||
def __init__(self, app) -> None:
|
||||
Murmur.ServerCallback.__init__(self)
|
||||
self.app = app
|
||||
|
||||
def userConnected(self, user, current=None) -> None:
|
||||
try:
|
||||
mumble_user = MumbleUser.objects.get(username=user)
|
||||
mumble_user.release = user.release
|
||||
mumble_user.version = user.version
|
||||
mumble_user.last_connect = datetime.now()
|
||||
mumble_user.save()
|
||||
except MumbleUser.DoesNotExist as a:
|
||||
try:
|
||||
mumble_user = TempUser.objects.get(username=user)
|
||||
mumble_user.release = user.release
|
||||
mumble_user.version = user.version
|
||||
mumble_user.last_connect = datetime.now()
|
||||
mumble_user.save()
|
||||
except Exception as b:
|
||||
logger.exception(a)
|
||||
logger.exception(b)
|
||||
|
||||
def userDisconnected(self, user, current=None) -> None:
|
||||
try:
|
||||
mumble_user = MumbleUser.objects.get(username=user)
|
||||
mumble_user.last_disconnect = datetime.now()
|
||||
mumble_user.save()
|
||||
except MumbleUser.DoesNotExist as a:
|
||||
try:
|
||||
mumble_user = TempUser.objects.get(username=user)
|
||||
mumble_user.last_disconnect = datetime.now()
|
||||
mumble_user.save()
|
||||
except Exception as b:
|
||||
logger.exception(a)
|
||||
logger.exception(b)
|
||||
|
||||
def userStateChanged(self, user, current=None) -> None:
|
||||
pass
|
||||
|
||||
def userTextMessage(self, user, text_message=None) -> None:
|
||||
if text_message.text == "!kicktemps":
|
||||
if self.server.hasPermission(user.session, 0, 0x10000):
|
||||
self.server.sendMessage(user.session, "Kicking all templink clients!")
|
||||
|
||||
users = self.server.getUsers()
|
||||
for (userid, auser) in users.items():
|
||||
if auser.userid > (server_config_obj.offset * 2):
|
||||
self.server.kickUser(auser.session, "Kicking all temp users! :-)")
|
||||
|
||||
self.server.sendMessage(user.session, "All templink clients kicked!")
|
||||
|
||||
else:
|
||||
self.server.sendMessage(user.session, "You do not have kick permissions!")
|
||||
|
||||
def channelCreated(self, channel, current=None) -> None:
|
||||
pass
|
||||
|
||||
def channelRemoved(self, channel, current=None) -> None:
|
||||
pass
|
||||
|
||||
def channelStateChanged(self, channel, current=None) -> None:
|
||||
pass
|
||||
|
||||
class AllianceAuthAuthenticator(Murmur.ServerUpdatingAuthenticator):
|
||||
texture_cache = {}
|
||||
|
||||
def __init__(self):
|
||||
Murmur.ServerUpdatingAuthenticator.__init__(self)
|
||||
|
||||
@fortifyIceFu(authenticateFortifyResult)
|
||||
@checkSecret
|
||||
def authenticate(self, name, pw, certlist, certhash, strong, current=None) -> tuple[int, str | None, str | None]:
|
||||
"""
|
||||
This function is called to authenticate a user
|
||||
"""
|
||||
FALL_THROUGH = -2
|
||||
AUTH_REFUSED = -1
|
||||
if name == "SuperUser":
|
||||
logger.debug("Forced fall through for SuperUser")
|
||||
return (FALL_THROUGH, None, None)
|
||||
|
||||
try:
|
||||
mumble_user = MumbleUser.objects.get(username=name)
|
||||
except MumbleUser.DoesNotExist:
|
||||
try:
|
||||
mumble_user = TempUser.objects.get(username=name)
|
||||
except TempUser.DoesNotExist:
|
||||
return (-2, None, None) # No Standard or Temp User
|
||||
|
||||
logger.debug("checking password with hash function: %s" % mumble_user.hashfn)
|
||||
if allianceauth_check_hash(pw, mumble_user.pwhash, mumble_user.hashfn):
|
||||
logger.info(f'User authenticated: {mumble_user.display_name} {mumble_user.user_id + server_config_obj.offset}')
|
||||
logger.debug("Group memberships: %s", mumble_user.group_string())
|
||||
return (mumble_user.user_id + server_config_obj.offset, mumble_user.display_name, mumble_user.group_string())
|
||||
logger.info(
|
||||
f'Failed authentication attempt for user: {name} {mumble_user.user_id + server_config_obj.offset}')
|
||||
return (AUTH_REFUSED, None, None)
|
||||
|
||||
@fortifyIceFu((False, None))
|
||||
@checkSecret
|
||||
def getInfo(self, id, current=None) -> tuple[bool, None]:
|
||||
"""
|
||||
Gets called to fetch user specific information
|
||||
"""
|
||||
# We do not expose any additional information so always fall through
|
||||
logger.debug("getInfo for %d -> denied", id)
|
||||
return (False, None)
|
||||
|
||||
@fortifyIceFu(-2)
|
||||
@checkSecret
|
||||
def nameToId(self, name, current=None) -> int:
|
||||
"""
|
||||
Gets called to get the id for a given username
|
||||
"""
|
||||
if name == "SuperUser":
|
||||
logger.debug("nameToId SuperUser -> forced fall through")
|
||||
return -2 # FALL_THROUGH
|
||||
|
||||
try:
|
||||
return (MumbleUser.objects.get(username=name).pk + server_config_obj.offset)
|
||||
except MumbleUser.DoesNotExist:
|
||||
try:
|
||||
return (TempUser.objects.get(username=name).pk + server_config_obj.offset * 2)
|
||||
except TempUser.DoesNotExist:
|
||||
return -2 # FALL_THROUGH
|
||||
|
||||
@fortifyIceFu("")
|
||||
@checkSecret
|
||||
def idToName(self, id, current=None) -> str:
|
||||
"""
|
||||
Gets called to get the username for a given id
|
||||
"""
|
||||
if id < server_config_obj.offset:
|
||||
return "" # FALL_THROUGH
|
||||
|
||||
try:
|
||||
mumble_user = MumbleUser.objects.get(user_id=id - server_config_obj.offset)
|
||||
mumble_user.username
|
||||
except MumbleUser.DoesNotExist:
|
||||
try:
|
||||
mumble_user = TempUser.objects.get(user_id=id - server_config_obj.offset * 2)
|
||||
mumble_user.username
|
||||
except TempUser.DoesNotExist:
|
||||
return "" # FALL_THROUGH
|
||||
|
||||
# I dont quite rightly know why we have this
|
||||
# SuperUser shouldnt be in our Authenticator?
|
||||
# But Maybe it can be?
|
||||
if MumbleUser.objects.get(user_id=id - server_config_obj.offset).username == "SuperUser":
|
||||
logger.debug('idToName %d -> "SuperUser" caught')
|
||||
return "" # FALL_THROUGH
|
||||
else:
|
||||
return mumble_user.username
|
||||
|
||||
@fortifyIceFu("")
|
||||
@checkSecret
|
||||
def idToTexture(self, id, current=None):
|
||||
"""
|
||||
Gets called to get the corresponding texture for a user
|
||||
"""
|
||||
if server_config_obj.avatar_enable is False:
|
||||
logger.debug(f"idToTexture {id} -> avatar display disabled, fall through")
|
||||
return "" # FALL_THROUGH
|
||||
|
||||
if id < server_config_obj.offset:
|
||||
return "" # FALL_THROUGH
|
||||
|
||||
try:
|
||||
avatar_url = MumbleUser.objects.get(user_id=id - server_config_obj.offset).user.profile.main_character.portrait_url()
|
||||
except MumbleUser.DoesNotExist:
|
||||
logger.debug(f"idToTexture {id} -> MumbleUser.DoesNotExist, Fall Through")
|
||||
return "" # FALL_THROUGH
|
||||
|
||||
if avatar_url:
|
||||
if avatar_url in self.texture_cache:
|
||||
logger.debug('idToTexture {id} -> cached avatar returned: {avatar_url}')
|
||||
return self.texture_cache[avatar_url]
|
||||
|
||||
# Not cached? Try to retrieve from CCP image server.
|
||||
try:
|
||||
logger.debug('idToTexture %d -> try file "%s"', id, avatar_url)
|
||||
handle = urlopen(avatar_url)
|
||||
|
||||
except (OSError, Exception) as e:
|
||||
logger.exception(e)
|
||||
logger.debug(f'idToTexture {id} -> image download for {avatar_url} failed, fall through')
|
||||
return "" # FALL_THROUGH
|
||||
else:
|
||||
file = handle.read()
|
||||
handle.close()
|
||||
|
||||
# Cache resulting avatar by file address and return image.
|
||||
self.texture_cache[avatar_url] = file
|
||||
logger.debug(f'idToTexture {id} -> avatar from {avatar_url} retrieved and returned')
|
||||
return self.texture_cache[avatar_url]
|
||||
|
||||
else:
|
||||
logger.debug(f"idToTexture {id} -> empty avatar_url, final fall through")
|
||||
return "" # FALL_THROUGH
|
||||
|
||||
@fortifyIceFu(-2)
|
||||
@checkSecret
|
||||
def registerUser(self, name, current=None) -> int:
|
||||
"""
|
||||
Gets called when the server is asked to register a user.
|
||||
"""
|
||||
logger.debug(f'registerUser {name} -> fall through')
|
||||
return -2 # FALL_THROUGH
|
||||
|
||||
@fortifyIceFu(-1)
|
||||
@checkSecret
|
||||
def unregisterUser(self, id, current=None) -> int:
|
||||
"""
|
||||
Gets called when the server is asked to unregister a user.
|
||||
"""
|
||||
# Return -1 to fall through to internal server database, so as to not modify Alliance Auth
|
||||
# but we can make murmur delete all additional logger.information it got this way.
|
||||
logger.debug(f"unregisterUser {id} -> fall through", )
|
||||
return -1 # FALL_THROUGH
|
||||
|
||||
@fortifyIceFu({})
|
||||
@checkSecret
|
||||
def getRegisteredUsers(self, filter, current=None) -> dict:
|
||||
"""
|
||||
Returns a list of usernames in the AllianceAuth database which contain
|
||||
filter as a substring.
|
||||
"""
|
||||
|
||||
if not filter:
|
||||
mumble_users = MumbleUser.objects.all()
|
||||
else:
|
||||
mumble_users = MumbleUser.objects.filter(username__icontains=filter)
|
||||
|
||||
if not mumble_users.exists():
|
||||
logger.debug(f'getRegisteredUsers -> empty list for filter {filter}', )
|
||||
return {}
|
||||
logger.debug(f'getRegisteredUsers -> {len(mumble_users)} results for filter {filter}')
|
||||
|
||||
return {mumble_user.user_id + server_config_obj.offset: mumble_user.username for mumble_user in mumble_users}
|
||||
|
||||
@fortifyIceFu(-1)
|
||||
@checkSecret
|
||||
def setInfo(self, id, info, current=None) -> int:
|
||||
"""
|
||||
Gets called when the server is supposed to save additional information
|
||||
about a user to his database
|
||||
"""
|
||||
# Return -1 to fall through to the internal server handler.
|
||||
# Store this in Murmur, Not Alliance Auth
|
||||
logger.debug(f"setInfo {id} -> fall through")
|
||||
return -1 # FALL_THROUGH
|
||||
|
||||
@fortifyIceFu(-1)
|
||||
@checkSecret
|
||||
def setTexture(self, id, texture, current=None) -> int:
|
||||
"""
|
||||
Gets called when the server is asked to update the user texture of a user
|
||||
"""
|
||||
# Return -1 to fall through to the internal server handler.
|
||||
# Store this in Murmur, Not Alliance Auth
|
||||
logger.debug(f"setTexture {id} -> fall through")
|
||||
return -1 # FALL_THROUGH
|
||||
|
||||
#
|
||||
# --- Start of authenticator
|
||||
#
|
||||
logger.info(f"Starting AllianceAuth Mumble Authenticator V:{__version__}")
|
||||
initdata = Ice.InitializationData()
|
||||
initdata.properties = Ice.createProperties([], initdata.properties)
|
||||
|
||||
initdata.properties.setProperty('Ice.ImplicitContext', 'Shared')
|
||||
initdata.properties.setProperty('Ice.Default.EncodingVersion', '1.0')
|
||||
initdata.logger = logger
|
||||
|
||||
app = AllianceAuthAuthenticatorApp()
|
||||
state = app.main(sys.argv, initData=initdata)
|
||||
logger.info('Shutdown complete')
|
||||
|
||||
|
||||
def allianceauth_check_hash(password, hash, hash_type) -> bool:
|
||||
"""
|
||||
:param password: Password to be verified
|
||||
:param hash: Hash for the password to be checked against
|
||||
:param hash_type: Hashing function originally used to generate the hash
|
||||
"""
|
||||
if hash_type == "sha1":
|
||||
return sha1(password).hexdigest() == hash
|
||||
elif hash_type == "bcrypt-sha256":
|
||||
return bcrypt_sha256.verify(password, hash)
|
||||
else:
|
||||
logger.warning("No valid hash function found for %s" % hash_type)
|
||||
return False
|
||||
|
||||
|
||||
def idler_handler(server) -> None:
|
||||
logger.debug("IdlerHandler: Starting")
|
||||
users = server.getUsers().values()
|
||||
logger.debug("IdleHandler: Fetched All Users")
|
||||
for user in users:
|
||||
logger.debug(f"IdleHandler: Checking user {user.name}")
|
||||
if isinstance(user, int):
|
||||
logger.debug(f"IdleHandler: Skipping User {user.name}, This happens occasionally")
|
||||
continue
|
||||
|
||||
if user_idlesecs > server_config_obj.idler_handler.seconds:
|
||||
logger.debug(
|
||||
f"IdleHandler: User {user.name} is AFK, for {user_idlesecs}/{server_config_obj.idler_handler.seconds}"
|
||||
)
|
||||
state = server.getState(user.session)
|
||||
if state:
|
||||
# AllowList > DenyList
|
||||
if server_config_obj.idler_handler.denylist is False:
|
||||
if state.channel not in server_config_obj.idler_handler.list:
|
||||
return
|
||||
elif server_config_obj.idler_handler.denylist is True:
|
||||
if state.channel in server_config_obj.idler_handler.list:
|
||||
return
|
||||
|
||||
if state.channel == server_config_obj.idler_handler.channel:
|
||||
return
|
||||
|
||||
state.channel = server_config_obj.idler_handler.channel
|
||||
state.selfMute = True
|
||||
state.selfDeaf = True
|
||||
server.setState(state)
|
||||
logger.debug(f"IdleHandler: Moved AFK User {user.name}")
|
||||
|
||||
Timer(server_config_obj.idler_handler.interval, idler_handler, (server,)).start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -0,0 +1,15 @@
|
||||
from allianceauth.services.modules.mumble import authenticator
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Run the Mumble Authenticator'
|
||||
|
||||
def add_arguments(self, parser) -> None:
|
||||
parser.add_argument(
|
||||
'--server_id', action='store', default=1,
|
||||
help='Run the Mumble Authenticator for the given Server Configuration ID')
|
||||
|
||||
def handle(self, *args, **options) -> None:
|
||||
authenticator.main()
|
48
allianceauth/services/modules/mumble/managers.py
Normal file
48
allianceauth/services/modules/mumble/managers.py
Normal file
@ -0,0 +1,48 @@
|
||||
import random
|
||||
import string
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from allianceauth.services.hooks import NameFormatter
|
||||
from passlib.hash import bcrypt_sha256
|
||||
from django.db import models
|
||||
from allianceauth.services.abstract import AbstractServiceModel
|
||||
import logging
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MumbleManager(models.Manager):
|
||||
@staticmethod
|
||||
def get_username(user) -> str:
|
||||
return user.profile.main_character.character_name # main character as the user.username may be incorrect
|
||||
|
||||
@staticmethod
|
||||
def sanitise_username(username) -> str:
|
||||
return username.replace(" ", "_")
|
||||
|
||||
@staticmethod
|
||||
def generate_random_pass() -> str:
|
||||
return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)])
|
||||
|
||||
@staticmethod
|
||||
def gen_pwhash(password) -> str:
|
||||
return bcrypt_sha256.encrypt(password.encode('utf-8'))
|
||||
|
||||
def create(self, user):
|
||||
try:
|
||||
username = self.get_username(user)
|
||||
logger.debug(f"Creating mumble user with username {username}")
|
||||
username_clean = self.sanitise_username(username)
|
||||
password = self.generate_random_pass()
|
||||
pwhash = self.gen_pwhash(password)
|
||||
logger.debug("Proceeding with mumble user creation: clean username {}, pwhash starts with {}".format(
|
||||
username_clean, pwhash[0:5]))
|
||||
logger.info(f"Creating mumble user {username_clean}")
|
||||
|
||||
result = super().create(user=user, username=username_clean, pwhash=pwhash)
|
||||
result.credentials.update({'username': result.username, 'password': password})
|
||||
return result
|
||||
except AttributeError: # No Main or similar errors
|
||||
return False
|
||||
return False
|
@ -0,0 +1,128 @@
|
||||
# Generated by Django 5.2.1 on 2025-07-07 04:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('eveonline', '0019_v5squash'),
|
||||
('mumble', '0015_v5squash'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IdlerHandler',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, verbose_name='Name')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('seconds', models.SmallIntegerField(default=3600, verbose_name='Idle Seconds')),
|
||||
('interval', models.SmallIntegerField(default=60, verbose_name='Run Interval')),
|
||||
('channel', models.SmallIntegerField(verbose_name='AFK Channel')),
|
||||
('denylist', models.BooleanField(default=True, help_text='True for DenyList, False for Allowlist', verbose_name='DenyList')),
|
||||
('list', models.CharField(default='', max_length=50, verbose_name='Allow/Deny list')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Idler Handler',
|
||||
'verbose_name_plural': 'Idler Handlers',
|
||||
'default_permissions': (),
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='mumbleuser',
|
||||
options={'permissions': (('access_mumble', 'Can access the Mumble service'), ('view_connection_history', 'Can access the connection history of the Mumble service')), 'verbose_name': 'User', 'verbose_name_plural': 'Users'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='mumbleuser',
|
||||
name='display_name',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='mumbleuser',
|
||||
name='groups',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mumbleuser',
|
||||
name='certhash',
|
||||
field=models.CharField(blank=True, editable=False, help_text='Hash of Mumble client certificate as presented when user authenticates', max_length=254, null=True, verbose_name='Certificate Hash'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mumbleuser',
|
||||
name='hashfn',
|
||||
field=models.CharField(choices=[('bcrypt-sha256', 'SHA256'), ('sha1', 'SHA1')], default='bcrypt-sha256', max_length=15),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mumbleuser',
|
||||
name='last_connect',
|
||||
field=models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Connection to Mumble', null=True, verbose_name='Last Connection'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mumbleuser',
|
||||
name='last_disconnect',
|
||||
field=models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Disconnection from Mumble', null=True, verbose_name='Last Disconnection'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mumbleuser',
|
||||
name='release',
|
||||
field=models.TextField(blank=True, editable=False, help_text='Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else.', null=True, verbose_name='Mumble Release'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MumbleServerServer',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('ip', models.GenericIPAddressField(default='127.0.0.1', verbose_name='Host IP Address')),
|
||||
('endpoint', models.GenericIPAddressField(default='127.0.0.1', verbose_name='Endpoint IP Address')),
|
||||
('port', models.PositiveSmallIntegerField(default=6502, verbose_name='Port')),
|
||||
('secret', models.CharField(max_length=50, verbose_name='ICE Secret')),
|
||||
('watchdog', models.SmallIntegerField(default=30, verbose_name='Watchdog Interval')),
|
||||
('slice', models.CharField(choices=[('MumbleServer.ice', 'MumbleServer.ice (Mumble >=1.5.17)'), ('Murmur.Ice', 'Murmur.Ice (Mumble <=1.5.17)')], default='MumbleServer.ice', max_length=16)),
|
||||
('virtual_servers', models.CharField(default='1', max_length=50, verbose_name='Virtual Servers')),
|
||||
('avatar_enable', models.BooleanField(default=True, verbose_name='Enable EVE Avatars')),
|
||||
('reject_on_error', models.BooleanField(default=True, verbose_name='Reject Unauthenticated')),
|
||||
('offset', models.IntegerField(default=1000000000, verbose_name='ID Offset')),
|
||||
('idler_handler', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='mumble.idlerhandler', verbose_name='Idler Handler')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Mumble Server',
|
||||
'verbose_name_plural': 'Mumble Servers',
|
||||
'default_permissions': (),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TempLink',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('expires', models.DateTimeField(verbose_name='Expiry')),
|
||||
('link_ref', models.CharField(max_length=20)),
|
||||
('creator', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='eveonline.evecharacter')),
|
||||
],
|
||||
options={
|
||||
'permissions': (('create_new_links', 'Can Create Temp Links'),),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TempUser',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('username', models.CharField(max_length=254, unique=True)),
|
||||
('pwhash', models.CharField(max_length=90)),
|
||||
('hashfn', models.CharField(choices=[('bcrypt-sha256', 'SHA256'), ('sha1', 'SHA1')], default='bcrypt-sha256', max_length=15)),
|
||||
('certhash', models.CharField(blank=True, editable=False, help_text='Hash of Mumble client certificate as presented when user authenticates', max_length=254, null=True, verbose_name='Certificate Hash')),
|
||||
('release', models.TextField(blank=True, editable=False, help_text='Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else.', null=True, verbose_name='Mumble Release')),
|
||||
('version', models.IntegerField(blank=True, editable=False, help_text='Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203.', null=True, verbose_name='Mumble Version')),
|
||||
('last_connect', models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Connection to Mumble', null=True, verbose_name='Last Connection')),
|
||||
('last_disconnect', models.DateTimeField(blank=True, editable=False, help_text='Timestamp of the users Last Disconnection from Mumble', null=True, verbose_name='Last Disconnection')),
|
||||
('expires', models.DateTimeField(verbose_name='Expiry')),
|
||||
('character_id', models.IntegerField(blank=True, default=None, null=True)),
|
||||
('templink', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='mumble.templink')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Temp User',
|
||||
'verbose_name_plural': 'Temp Users',
|
||||
'permissions': (),
|
||||
},
|
||||
),
|
||||
]
|
@ -2,130 +2,80 @@ import logging
|
||||
import random
|
||||
import string
|
||||
from typing import ClassVar
|
||||
from passlib.hash import bcrypt_sha256
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db import models
|
||||
|
||||
from allianceauth.services.abstract import AbstractServiceModel
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from allianceauth.services.hooks import NameFormatter
|
||||
from allianceauth.services.modules.mumble.managers import MumbleManager
|
||||
from passlib.hash import bcrypt_sha256
|
||||
from django.db import models
|
||||
from allianceauth.services.abstract import AbstractServiceModel
|
||||
import logging
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MumbleManager(models.Manager):
|
||||
HASH_FN = "bcrypt-sha256"
|
||||
|
||||
@staticmethod
|
||||
def get_display_name(user) -> str:
|
||||
from .auth_hooks import MumbleService
|
||||
|
||||
return NameFormatter(MumbleService(), user).format_name()
|
||||
|
||||
@staticmethod
|
||||
def get_username(user):
|
||||
return user.profile.main_character.character_name # main character as the user.username may be incorrect
|
||||
|
||||
@staticmethod
|
||||
def sanitise_username(username):
|
||||
return username.replace(" ", "_")
|
||||
|
||||
@staticmethod
|
||||
def generate_random_pass():
|
||||
return "".join([random.choice(string.ascii_letters + string.digits) for n in range(16)])
|
||||
|
||||
@staticmethod
|
||||
def gen_pwhash(password):
|
||||
return bcrypt_sha256.encrypt(password.encode("utf-8"))
|
||||
|
||||
def create(self, user):
|
||||
try:
|
||||
username = self.get_username(user)
|
||||
logger.debug(f"Creating mumble user with username {username}")
|
||||
username_clean = self.sanitise_username(username)
|
||||
display_name = self.get_display_name(user)
|
||||
password = self.generate_random_pass()
|
||||
pwhash = self.gen_pwhash(password)
|
||||
logger.debug(
|
||||
f"Proceeding with mumble user creation: clean username {username_clean}, pwhash starts with {pwhash[0:5]}"
|
||||
)
|
||||
logger.info(f"Creating mumble user {username_clean}")
|
||||
|
||||
result = super().create(
|
||||
user=user, username=username_clean, pwhash=pwhash, hashfn=self.HASH_FN, display_name=display_name
|
||||
)
|
||||
result.update_groups()
|
||||
result.credentials.update({"username": result.username, "password": password})
|
||||
return result
|
||||
except AttributeError: # No Main or similar errors
|
||||
return False
|
||||
return False
|
||||
|
||||
def user_exists(self, username) -> bool:
|
||||
return self.filter(username=username).exists()
|
||||
|
||||
|
||||
class MumbleUser(AbstractServiceModel):
|
||||
user = models.OneToOneField("auth.User", primary_key=True, on_delete=models.CASCADE, related_name="mumble")
|
||||
class HashFunction(models.TextChoices):
|
||||
SHA256 = 'bcrypt-sha256', _('SHA256')
|
||||
SHA1 = 'sha1', _('SHA1')
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
primary_key=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='mumble'
|
||||
)
|
||||
username = models.CharField(max_length=254, unique=True)
|
||||
pwhash = models.CharField(max_length=90)
|
||||
hashfn = models.CharField(max_length=20, default="sha1")
|
||||
groups = models.TextField(blank=True, default="")
|
||||
hashfn = models.CharField(
|
||||
max_length=15,
|
||||
choices=HashFunction.choices,
|
||||
default=HashFunction.SHA256)
|
||||
certhash = models.CharField(
|
||||
verbose_name="Certificate Hash",
|
||||
max_length=254,
|
||||
blank=True,
|
||||
default="",
|
||||
editable=False,
|
||||
help_text="Hash of Mumble client certificate as presented when user authenticates",
|
||||
)
|
||||
display_name = models.CharField(max_length=254, unique=True)
|
||||
max_length=254,blank=True, null=True, editable=False,
|
||||
help_text="Hash of Mumble client certificate as presented when user authenticates")
|
||||
release = models.TextField(
|
||||
verbose_name="Mumble Release",
|
||||
max_length=254,
|
||||
blank=True,
|
||||
editable=False,
|
||||
default="",
|
||||
help_text="Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else.",
|
||||
)
|
||||
blank=True, null=True, editable=False,
|
||||
help_text="Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else.")
|
||||
version = models.IntegerField(
|
||||
verbose_name="Mumble Version",
|
||||
blank=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
help_text="Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203.",
|
||||
)
|
||||
blank=True, null=True, editable=False,
|
||||
help_text="Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203.")
|
||||
last_connect = models.DateTimeField(
|
||||
verbose_name="Last Connection",
|
||||
max_length=254,
|
||||
blank=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
help_text="Timestamp of the users Last Connection to Mumble",
|
||||
)
|
||||
blank=True, null=True, editable=False,
|
||||
help_text="Timestamp of the users Last Connection to Mumble")
|
||||
last_disconnect = models.DateTimeField(
|
||||
verbose_name="Last Disconnection",
|
||||
max_length=254,
|
||||
blank=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
help_text="Timestamp of the users Last Disconnection to Mumble",
|
||||
)
|
||||
blank=True, null=True, editable=False,
|
||||
help_text="Timestamp of the users Last Disconnection from Mumble")
|
||||
|
||||
objects: ClassVar[MumbleManager] = MumbleManager()
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
from .auth_hooks import MumbleService
|
||||
return NameFormatter(MumbleService(), self.user).format_name()
|
||||
|
||||
@property
|
||||
def groups(self) -> str:
|
||||
# Not sure where this is used, there was a test for it
|
||||
return self.group_string()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.username
|
||||
return f"{self.username}"
|
||||
|
||||
def update_password(self, password=None) -> None:
|
||||
init_password = password
|
||||
logger.debug("Updating mumble user %s password.")
|
||||
logger.debug(f"Updating mumble user {self.user} password.")
|
||||
if not password:
|
||||
password = MumbleManager.generate_random_pass()
|
||||
pwhash = MumbleManager.gen_pwhash(password)
|
||||
logger.debug(f"Proceeding with mumble user {self.user} password update - pwhash starts with {pwhash[0:5]}")
|
||||
self.pwhash = pwhash
|
||||
self.hashfn = MumbleManager.HASH_FN
|
||||
self.hashfn = self.HashFunction.SHA256
|
||||
self.save()
|
||||
if init_password is None:
|
||||
self.credentials.update({"username": self.username, "password": password})
|
||||
@ -133,27 +83,183 @@ class MumbleUser(AbstractServiceModel):
|
||||
def reset_password(self) -> None:
|
||||
self.update_password()
|
||||
|
||||
def update_groups(self, groups: Group = None):
|
||||
if groups is None:
|
||||
groups = self.user.groups.all()
|
||||
groups_str = [self.user.profile.state.name]
|
||||
for group in groups:
|
||||
groups_str.append(str(group.name))
|
||||
safe_groups = ",".join({g.replace(" ", "-") for g in groups_str})
|
||||
logger.info(f"Updating mumble user {self.user} groups to {safe_groups}")
|
||||
self.groups = safe_groups
|
||||
self.save()
|
||||
return True
|
||||
def group_string(self) -> str:
|
||||
"""Return a Mumble Safe Formatted List of Groups
|
||||
This used to be a ModelField, generated on the fly now with DjangoAuthenticatorTM
|
||||
|
||||
def update_display_name(self):
|
||||
logger.info(f"Updating mumble user {self.user} display name")
|
||||
self.display_name = MumbleManager.get_display_name(self.user)
|
||||
self.save()
|
||||
return True
|
||||
Returns:
|
||||
LiteralString: Mumble Safe Formatted List of Groups
|
||||
"""
|
||||
groups_str = [self.user.profile.state.name]
|
||||
for group in self.user.groups.all():
|
||||
groups_str.append(str(group.name))
|
||||
return ','.join({g.replace(' ', '-') for g in groups_str})
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
verbose_name = _("User")
|
||||
verbose_name_plural = _("Users")
|
||||
permissions = (
|
||||
("access_mumble", "Can access the Mumble service"),
|
||||
("view_connection_history", "Can access the connection history of the Mumble service"),
|
||||
)
|
||||
|
||||
|
||||
class TempLink(models.Model):
|
||||
expires = models.DateTimeField(_("Expiry"), auto_now=False, auto_now_add=False)
|
||||
link_ref = models.CharField(max_length=20)
|
||||
creator = models.ForeignKey(EveCharacter, on_delete=models.SET_NULL, null=True, default=None)
|
||||
|
||||
class Meta:
|
||||
permissions = (("create_new_links", "Can Create Temp Links"),)
|
||||
|
||||
def __str__(self):
|
||||
return f"Link {self.link_ref} - {self.expires}"
|
||||
|
||||
|
||||
class TempUser(models.Model):
|
||||
class HashFunction(models.TextChoices):
|
||||
SHA256 = 'bcrypt-sha256', _('SHA256')
|
||||
SHA1 = 'sha1', _('SHA1')
|
||||
name = models.CharField(max_length=200) # Display name to show
|
||||
username = models.CharField(max_length=254, unique=True)
|
||||
pwhash = models.CharField(max_length=90)
|
||||
hashfn = models.CharField(
|
||||
max_length=15,
|
||||
choices=HashFunction.choices,
|
||||
default=HashFunction.SHA256)
|
||||
certhash = models.CharField(
|
||||
verbose_name="Certificate Hash",
|
||||
max_length=254,blank=True, null=True, editable=False,
|
||||
help_text="Hash of Mumble client certificate as presented when user authenticates")
|
||||
release = models.TextField(
|
||||
verbose_name="Mumble Release",
|
||||
blank=True, null=True, editable=False,
|
||||
help_text="Client release. For official releases, this equals the version. For snapshots and git compiles, this will be something else.")
|
||||
version = models.IntegerField(
|
||||
verbose_name="Mumble Version",
|
||||
blank=True, null=True, editable=False,
|
||||
help_text="Client version. Major version in upper 16 bits, followed by 8 bits of minor version and 8 bits of patchlevel. Version 1.2.3 = 0x010203.")
|
||||
last_connect = models.DateTimeField(
|
||||
verbose_name="Last Connection",
|
||||
blank=True, null=True, editable=False,
|
||||
help_text="Timestamp of the users Last Connection to Mumble")
|
||||
last_disconnect = models.DateTimeField(
|
||||
verbose_name="Last Disconnection",
|
||||
blank=True, null=True, editable=False,
|
||||
help_text="Timestamp of the users Last Disconnection from Mumble")
|
||||
expires = models.DateTimeField(_("Expiry"), auto_now=False, auto_now_add=False)
|
||||
|
||||
templink = models.ForeignKey(TempLink, on_delete=models.CASCADE, null=True, default=None)
|
||||
character_id = models.IntegerField(default=None, blank=True, null=True)
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
from .auth_hooks import MumbleService
|
||||
return NameFormatter(MumbleService(), self.user).format_name()
|
||||
|
||||
@property
|
||||
def groups(self) -> str:
|
||||
# Not sure where this is used, there was a test for it
|
||||
return self.group_string()
|
||||
|
||||
@staticmethod
|
||||
def get_username(user) -> str:
|
||||
return user.profile.main_character.character_name # main character as the user.username may be incorrect
|
||||
|
||||
@staticmethod
|
||||
def sanitise_username(username) -> str:
|
||||
return username.replace(" ", "_")
|
||||
|
||||
@staticmethod
|
||||
def generate_random_pass() -> str:
|
||||
return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)])
|
||||
|
||||
@staticmethod
|
||||
def gen_pwhash(password) -> str:
|
||||
return bcrypt_sha256.encrypt(password.encode('utf-8'))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Temp User: {self.username} - {self.name}"
|
||||
|
||||
def group_string(self) -> str:
|
||||
"""Overwritten from MumbleUser, we could add features to this in the future
|
||||
"""
|
||||
return str(["Guest"])
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Temp User")
|
||||
verbose_name_plural = _("Temp Users")
|
||||
permissions = ()
|
||||
|
||||
|
||||
class IdlerHandler(models.Model):
|
||||
name = models.CharField(_("Name"), max_length=50)
|
||||
enabled = models.BooleanField(_("Enabled"), default=False)
|
||||
seconds = models.SmallIntegerField(_("Idle Seconds"), default=3600)
|
||||
interval = models.SmallIntegerField(_("Run Interval"), default=60)
|
||||
channel = models.SmallIntegerField(_("AFK Channel"))
|
||||
denylist = models.BooleanField(_("DenyList"), default=True, help_text="True for DenyList, False for Allowlist")
|
||||
list = models.CharField(_("Allow/Deny list"), max_length=50, default="")
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Idler Handler")
|
||||
verbose_name_plural = _("Idler Handlers")
|
||||
default_permissions = ()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
class MumbleServerServer(models.Model): # This will clash with ICE MumbleServer
|
||||
class ServerVersion(models.TextChoices):
|
||||
MUMBLESERVER_ICE = 'MumbleServer.ice', _('MumbleServer.ice (Mumble >=1.5.17)')
|
||||
MURMUR_ICE = 'Murmur.Ice', _('Murmur.Ice (Mumble <=1.5.17)')
|
||||
name = models.CharField(max_length=50)
|
||||
ip = models.GenericIPAddressField(
|
||||
verbose_name=_("Host IP Address"),
|
||||
protocol="both",
|
||||
unpack_ipv4=False,
|
||||
default="127.0.0.1")
|
||||
endpoint = models.GenericIPAddressField(
|
||||
verbose_name=_("Endpoint IP Address"),
|
||||
protocol="both",
|
||||
unpack_ipv4=False,
|
||||
default="127.0.0.1")
|
||||
|
||||
port = models.PositiveSmallIntegerField(verbose_name=_("Port"), default=6502)
|
||||
secret = models.CharField(verbose_name=_("ICE Secret"), max_length=50)
|
||||
watchdog = models.SmallIntegerField(verbose_name=_("Watchdog Interval"), default=30)
|
||||
slice = models.CharField(
|
||||
max_length=16,
|
||||
choices=ServerVersion.choices,
|
||||
default=ServerVersion.MUMBLESERVER_ICE)
|
||||
virtual_servers = models.CharField(
|
||||
verbose_name=_("Virtual Servers"),
|
||||
max_length=50,
|
||||
default="1")
|
||||
avatar_enable = models.BooleanField(
|
||||
verbose_name=_("Enable EVE Avatars"),
|
||||
default=True)
|
||||
reject_on_error = models.BooleanField(
|
||||
verbose_name=_("Reject Unauthenticated"),
|
||||
default=True)
|
||||
offset = models.IntegerField(
|
||||
verbose_name=_("ID Offset"),
|
||||
default=1000000000)
|
||||
idler_handler = models.ForeignKey(
|
||||
IdlerHandler,
|
||||
verbose_name=_("Idler Handler"),
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Mumble Server")
|
||||
verbose_name_plural = _("Mumble Servers")
|
||||
default_permissions = ()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}"
|
||||
|
||||
def virtual_servers_list(self) -> list:
|
||||
return [int(num) for num in self.virtual_servers.replace(",", " ").split() if num]
|
||||
|
@ -1,13 +1,13 @@
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from celery import shared_task
|
||||
from .models import MumbleUser, TempLink, TempUser
|
||||
from celery import shared_task
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from allianceauth.services.tasks import QueueOnce
|
||||
|
||||
from .models import MumbleUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -28,56 +28,9 @@ class MumbleTasks:
|
||||
logger.info("Deleting all MumbleUser models")
|
||||
MumbleUser.objects.all().delete()
|
||||
|
||||
@staticmethod
|
||||
@shared_task(bind=True, name="mumble.update_groups", base=QueueOnce)
|
||||
def update_groups(self, pk):
|
||||
user = User.objects.get(pk=pk)
|
||||
logger.debug(f"Updating mumble groups for user {user}")
|
||||
if MumbleTasks.has_account(user):
|
||||
try:
|
||||
if not user.mumble.update_groups():
|
||||
raise Exception("Group sync failed")
|
||||
logger.debug(f"Updated user {user} mumble groups.")
|
||||
return True
|
||||
except MumbleUser.DoesNotExist:
|
||||
logger.info(f"Mumble group sync failed for {user}, user does not have a mumble account")
|
||||
except Exception as e:
|
||||
logger.exception(f"Mumble group sync failed for {user}, retrying in 10 mins")
|
||||
raise self.retry(exc=e, countdown=60 * 10) from e
|
||||
else:
|
||||
logger.debug(f"User {user} does not have a mumble account, skipping")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@shared_task(bind=True, name="mumble.update_display_name", base=QueueOnce)
|
||||
def update_display_name(self, pk):
|
||||
user = User.objects.get(pk=pk)
|
||||
logger.debug(f"Updating mumble groups for user {user}")
|
||||
if MumbleTasks.has_account(user):
|
||||
try:
|
||||
if not user.mumble.update_display_name():
|
||||
raise Exception("Display Name Sync failed")
|
||||
logger.debug(f"Updated user {user} mumble display name.")
|
||||
return True
|
||||
except MumbleUser.DoesNotExist:
|
||||
logger.info(f"Mumble display name sync failed for {user}, user does not have a mumble account")
|
||||
except Exception as e:
|
||||
logger.exception(f"Mumble display name sync failed for {user}, retrying in 10 mins")
|
||||
raise self.retry(exc=e, countdown=60 * 10) from e
|
||||
else:
|
||||
logger.debug(f"User {user} does not have a mumble account, skipping")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@shared_task(name="mumble.update_all_groups")
|
||||
def update_all_groups():
|
||||
logger.debug("Updating ALL mumble groups")
|
||||
for mumble_user in MumbleUser.objects.exclude(username__exact=''):
|
||||
MumbleTasks.update_groups.delay(mumble_user.user.pk)
|
||||
|
||||
@staticmethod
|
||||
@shared_task(name="mumble.update_all_display_names")
|
||||
def update_all_display_names():
|
||||
logger.debug("Updating ALL mumble display names")
|
||||
for mumble_user in MumbleUser.objects.exclude(username__exact=''):
|
||||
MumbleTasks.update_display_name.delay(mumble_user.user.pk)
|
||||
@shared_task
|
||||
def tidy_up_temp_links() -> None:
|
||||
TempLink.objects.filter(expires__lt=datetime.now(timezone.utc).timestamp()).delete()
|
||||
TempUser.objects.filter(templink__isnull=True).delete()
|
||||
TempUser.objects.filter(expires__lt=datetime.now(timezone.utc).timestamp()).delete()
|
||||
|
@ -0,0 +1,19 @@
|
||||
{% extends "allianceauth/base-bs5.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
{% translate "Mumble Temporary Links" %}
|
||||
{% endblock page_title %}
|
||||
|
||||
{% block header_nav_brand %}
|
||||
{% translate "Mumble Temporary Links" %}
|
||||
{% endblock header_nav_brand %}
|
||||
|
||||
{% block content %}
|
||||
<div class="allianceauth-mumble">
|
||||
<div class="row">
|
||||
{% block mumble %}{% endblock mumble %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,120 @@
|
||||
{% extends "services/mumble/base.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block mumble %}
|
||||
<div class="text-center">
|
||||
<p>
|
||||
<img class="ra-avatar img-circle" src="{{ link.creator.portrait_url_128 }}" alt="{{ link.creator.character_name }}" />
|
||||
</p>
|
||||
|
||||
<h4>
|
||||
{% blocktranslatelate with character=link.creator.character_name %}
|
||||
{{ character }} has invited you to join Mumble!
|
||||
{% endblocktranslatelate %}
|
||||
</h4>
|
||||
|
||||
<p>
|
||||
{% translate "Time Remaining" %}: <span id="countdown{{ link.link_ref }}"></span>
|
||||
</p>
|
||||
<p>
|
||||
<span class="label label-info">{% translate "Link Ref" %}: {{ link.link_ref }}</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a class="btn btn-primary" href="mumble://{{ connect_url }}">
|
||||
<img src="{% static 'services/mumble/images/mumble-icon.png' %}" alt="{% translate 'Mumble' %}" height="64" width="64" style="margin: 15px" />
|
||||
|
||||
<span style="margin: 15px;">
|
||||
{% translate "Click to Join Mumble as" %}: <b>{{ temp_user.name }}</b>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10 col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
<div class="card-title mb-0">{% translate "Manually Connect" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<p>
|
||||
{% translate "If you have problems with the application link above, please use the following in Mumble's connection dialog." %}
|
||||
</p>
|
||||
<p>
|
||||
{% translate "Mumble URL" %}: <span class="badge bg-dark">{{ mumble }}</span>
|
||||
</p>
|
||||
<p>
|
||||
{% translate "Username" %}: <span class="badge bg-dark">{{ temp_user.username }}</span>
|
||||
</p>
|
||||
<p>
|
||||
{% translate "Password" %}: <span class="badge bg-dark">{{ temp_user.password }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "bundles/moment-js.html" with locale=True %}
|
||||
{% include "bundles/timers-js.html" %}
|
||||
|
||||
<script>
|
||||
/* global moment */
|
||||
const locale = '{{ LANGUAGE_CODE }}';
|
||||
const timers = [{
|
||||
'id': '{{ link.link_ref }}',
|
||||
'targetDate': moment('{{ link.expires| date:"c" }}'),
|
||||
'expired': false
|
||||
}];
|
||||
|
||||
moment.locale(locale);
|
||||
|
||||
/**
|
||||
* Update a timer
|
||||
*
|
||||
* @param timer
|
||||
*/
|
||||
const updateTimer = (timer) => {
|
||||
if (timer.targetDate.isAfter(Date.now())) {
|
||||
const duration = moment.duration(timer.targetDate - moment(), 'milliseconds');
|
||||
|
||||
document.getElementById("countdown" + timer.id).innerHTML = getDurationString(duration);
|
||||
} else {
|
||||
timer.expired = true;
|
||||
|
||||
document.getElementById("countdown" + timer.id).innerHTML = "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update all timers
|
||||
*/
|
||||
const updateAllTimers = () => {
|
||||
const l = timers.length;
|
||||
|
||||
for (let i = 0; i < l; ++i) {
|
||||
if (timers[i].expired) {
|
||||
continue;
|
||||
}
|
||||
|
||||
updateTimer(timers[i]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Timed update
|
||||
*/
|
||||
const timedUpdate = () => {
|
||||
updateAllTimers();
|
||||
};
|
||||
|
||||
// Set initial values
|
||||
timedUpdate();
|
||||
|
||||
// Start timed updates
|
||||
setInterval(timedUpdate, 1000);
|
||||
</script>
|
||||
{% endblock mumble %}
|
@ -0,0 +1,139 @@
|
||||
{% extends "services/mumble/base.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block mumble %}
|
||||
<div class="col-md-12 text-center">
|
||||
<div class="mumble-sso-login mb-3">
|
||||
<p>
|
||||
<img class="ra-avatar img-circle" src="{{ link.creator.portrait_url_128 }}" alt="{{ link.creator.character_name }}" />
|
||||
</p>
|
||||
|
||||
<h4>
|
||||
{% blocktranslatelate with character=link.creator.character_name %}
|
||||
{{ character }} has invited you to join Mumble!
|
||||
{% endblocktranslatelate %}
|
||||
</h4>
|
||||
|
||||
<p>
|
||||
{% translate "Time Remaining" %}: <span id="countdown{{ link.link_ref }}"></span>
|
||||
</p>
|
||||
<p>
|
||||
<span class="label label-info">{% translate "Link Ref" %}: {{ link.link_ref }}</span>
|
||||
</p>
|
||||
|
||||
<form action="{% url 'mumble:join' link.link_ref %}" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<input type="hidden" name="sso" value="True" />
|
||||
<input
|
||||
type="image"
|
||||
class="img-responsive center-block"
|
||||
src="{% static 'allianceauth/authentication/img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}"
|
||||
alt="{% translate 'Login with SSO"' %}"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mumble-non-sso-login">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10 col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
<div class="card-title mb-0">{% translate "Non SSO Login" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body text-start">
|
||||
<div class="form-group">
|
||||
<form action="{% url 'mumble:join' link.link_ref %}" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<input type="hidden" name="sso" value="False" />
|
||||
|
||||
<p class="text-center">
|
||||
{% translate "If you cannot SSO with your EVE character, You can enter your own details below." %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="name">{% translate "Name" %}</label>
|
||||
<input type="text" class="form-control" name="name" id="name" placeholder="{% translate 'Who are you?' %}" />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="association">{% translate "Association" %}</label>
|
||||
<input type="text" class="form-control" name="association" id="association" placeholder="{% translate 'Who are you with?' %}" />
|
||||
</p>
|
||||
|
||||
<button type="submit" value="{% translate 'Submit' %}" class="btn btn-primary">
|
||||
{% translate "Login Without SSO" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "bundles/moment-js.html" with locale=True %}
|
||||
{% include "bundles/timers-js.html" %}
|
||||
|
||||
<script>
|
||||
/* global moment */
|
||||
const locale = '{{ LANGUAGE_CODE }}';
|
||||
const timers = [{
|
||||
'id': '{{ link.link_ref }}',
|
||||
'targetDate': moment('{{ link.expires| date:"c" }}'),
|
||||
'expired': false
|
||||
}];
|
||||
|
||||
moment.locale(locale);
|
||||
|
||||
/**
|
||||
* Update a timer
|
||||
*
|
||||
* @param timer
|
||||
*/
|
||||
const updateTimer = (timer) => {
|
||||
if (timer.targetDate.isAfter(Date.now())) {
|
||||
const duration = moment.duration(timer.targetDate - moment(), 'milliseconds');
|
||||
|
||||
document.getElementById("countdown" + timer.id).innerHTML = getDurationString(duration);
|
||||
} else {
|
||||
timer.expired = true;
|
||||
|
||||
document.getElementById("countdown" + timer.id).innerHTML = "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update all timers
|
||||
*/
|
||||
const updateAllTimers = () => {
|
||||
const l = timers.length;
|
||||
|
||||
for (let i = 0; i < l; ++i) {
|
||||
if (timers[i].expired) {
|
||||
continue;
|
||||
}
|
||||
|
||||
updateTimer(timers[i]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Timed update
|
||||
*/
|
||||
const timedUpdate = () => {
|
||||
updateAllTimers();
|
||||
};
|
||||
|
||||
// Set initial values
|
||||
timedUpdate();
|
||||
|
||||
// Start timed updates
|
||||
setInterval(timedUpdate, 1000);
|
||||
</script>
|
||||
{% endblock mumble %}
|
@ -4,17 +4,17 @@
|
||||
|
||||
{% block title %}
|
||||
{{ service_name }}
|
||||
{% endblock %}
|
||||
{% endblock title %}
|
||||
|
||||
{% block url %}
|
||||
{% if username != '' %}
|
||||
<a href="mumble://{{ connect_url }}">{{ service_url }}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock url %}
|
||||
|
||||
{% block user %}
|
||||
{% include "services/service_username.html" with username=username %}
|
||||
{% endblock %}
|
||||
{% endblock user %}
|
||||
|
||||
{% block controls %}
|
||||
{% if username == "" %}
|
||||
|
@ -0,0 +1,197 @@
|
||||
{% extends "services/mumble/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block mumble %}
|
||||
<div class="col-md-6 mb-2">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-warning">
|
||||
<div class="card-title mb-0">{% translate "Mumble Temporary Links" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body text-center">
|
||||
<p>
|
||||
{% blocktranslate trimmed with bold_start="<b>" bold_end="</b>" %}
|
||||
Temp Links Give Access to mumble with the {{ bold_start }}Guest{{ bold_end }} Group for the preset time
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktranslate trimmed with bold_start="<b class='text-warning'>" bold_end="</b>" %}
|
||||
Connected users {{ bold_start }}will not{{ bold_end }} be kicked at the end of this period.
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktranslate trimmed with badge_start="<span class='badge bg-danger'>" badge_end="</span>" %}
|
||||
Templink users can be kicked in mumble by typing {{ badge_start }}!kicktemps{{ badge_end }} in mumble chat.
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktranslate trimmed with bold_start="<b class='text-warning'>" bold_end="</b>" %}
|
||||
There are {{ bold_start }}no restrictions{{ bold_end }} on who or how many can use a templink, share wisely.
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mt-sm-2 mt-md-0 mb-2">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">{% translate "Create New Link" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body text-center">
|
||||
<p>{% translate "Your link will be displayed on the next page for an easy copy and paste." %}</p>
|
||||
|
||||
<form action="{% url 'mumble:index' %}" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="time">{% translate "Duration (hours)" %}</label>
|
||||
<select class="form-select" name="time" id="time">
|
||||
<option value="3">3</option>
|
||||
<option value="6">6</option>
|
||||
<option value="12">12</option>
|
||||
<option value="24">24</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<input class="btn btn-primary" type="submit" value="{% translate 'OK' %}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if tl %}
|
||||
<div class="my-2">
|
||||
<div class="card">
|
||||
<div class="card-header bg-success">
|
||||
<div class="card-title mb-0">{% translate "New Link" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body text-center" style="min-height: 100px;">
|
||||
<p>
|
||||
{% translate "Expires in" %}: <span id="countdown{{ tl.link_ref }}hot"></span>
|
||||
</p>
|
||||
|
||||
<pre>{{ SITE_URL }}{% url 'mumble:join' tl.link_ref %}</pre>
|
||||
|
||||
<button class="btn btn-info" id="clipboard-new" data-clipboard-text="{{ SITE_URL }}{% url 'mumble:join' tl.link_ref %}">
|
||||
{% translate "Copy to Clipboard!" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="my-2">
|
||||
<div class="card pb-2">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">{% translate "Active Links" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body" style="min-height: 100px;">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="text-center">{% translate "Creator" %}</th>
|
||||
<th class="text-center">{% translate "Key" %}</th>
|
||||
<th class="text-center">{% translate "Time Left" %}</th>
|
||||
<th class="text-center">{% translate "Copy" %}</th>
|
||||
<th class="text-center">{% translate "Nuke" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for lnk in tl_list %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar img-circle" src="{{ lnk.creator.portrait_url_32 }}" alt="{{ lnk.creator.character_name }}" />
|
||||
</td>
|
||||
|
||||
<td class="text-center">{{ lnk.creator.character_name }}</td>
|
||||
|
||||
<td class="text-center">{{ lnk.link_ref }}</td>
|
||||
|
||||
<td class="text-center" id="countdown{{ lnk.link_ref }}"></td>
|
||||
|
||||
<td class="text-center">
|
||||
<button class="btn btn-info"
|
||||
id="clipboard-{{ lnk.link_ref }}"
|
||||
data-clipboard-text="{{ SITE_URL }}{% url 'mumble:join' lnk.link_ref %}">
|
||||
{% translate "Copy to Clipboard!" %}
|
||||
</button>
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<a class="btn btn-danger" href="{% url 'mumble:nuke' lnk.link_ref %}">{% translate "Nuke Link!" %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if ex_tl_list.count > 0 %}
|
||||
<div class="my-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title mb-0">{% translate "Expired Links" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body" style="min-height: 100px;">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="text-center">{% translate "Creator" %}</th>
|
||||
<th class="text-center">{% translate "Key" %}</th>
|
||||
<th class="text-center">{% translate "Nuke" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for lnk in ex_tl_list %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar img-circle" src="{{ lnk.creator.portrait_url_32 }}" alt="{{ lnk.creator.character_name }}" />
|
||||
</td>
|
||||
|
||||
<td class="text-center">{{ lnk.creator.character_name }}</td>
|
||||
|
||||
<td class="text-center">{{ lnk.link_ref }}</td>
|
||||
|
||||
<td class="text-center">
|
||||
<a class="btn btn-danger" href="{% url 'mumble:nuke' lnk.link_ref %}">{% translate "Nuke Link!" %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock mumble %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
{% include "bundles/clipboard-js.html" %}
|
||||
|
||||
<script>
|
||||
/* global ClipboardJS */
|
||||
const clipboard = new ClipboardJS('#clipboard-new');
|
||||
|
||||
{% for lnk in tl_list %}
|
||||
const lnk{{ lnk.link_ref }} = new ClipboardJS('#clipboard-{{ lnk.link_ref }}');
|
||||
{% endfor %}
|
||||
</script>
|
||||
|
||||
{% endblock extra_javascript %}
|
@ -1,211 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
from django import urls
|
||||
from django.contrib.auth.models import Group, Permission, User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from .auth_hooks import MumbleService
|
||||
from .models import MumbleUser
|
||||
from .tasks import MumbleTasks
|
||||
|
||||
MODULE_PATH = 'allianceauth.services.modules.mumble'
|
||||
DEFAULT_AUTH_GROUP = 'Member'
|
||||
|
||||
|
||||
def add_permissions():
|
||||
permission = Permission.objects.get(codename='access_mumble')
|
||||
members = Group.objects.get_or_create(name=DEFAULT_AUTH_GROUP)[0]
|
||||
AuthUtils.add_permissions_to_groups([permission], [members])
|
||||
|
||||
|
||||
class MumbleHooksTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.member = 'member_user'
|
||||
member = AuthUtils.create_member(self.member)
|
||||
AuthUtils.add_main_character(member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation', corp_ticker='TESTR')
|
||||
member = User.objects.get(pk=member.pk)
|
||||
MumbleUser.objects.create(user=member)
|
||||
self.none_user = 'none_user'
|
||||
AuthUtils.create_user(self.none_user)
|
||||
self.service = MumbleService
|
||||
add_permissions()
|
||||
|
||||
def test_has_account(self):
|
||||
member = User.objects.get(username=self.member)
|
||||
none_user = User.objects.get(username=self.none_user)
|
||||
self.assertTrue(MumbleTasks.has_account(member))
|
||||
self.assertFalse(MumbleTasks.has_account(none_user))
|
||||
|
||||
def test_service_enabled(self):
|
||||
service = self.service()
|
||||
member = User.objects.get(username=self.member)
|
||||
none_user = User.objects.get(username=self.none_user)
|
||||
|
||||
self.assertTrue(service.service_active_for_user(member))
|
||||
self.assertFalse(service.service_active_for_user(none_user))
|
||||
|
||||
@mock.patch(MODULE_PATH + '.tasks.User.mumble')
|
||||
def test_update_all_groups(self, mumble):
|
||||
service = self.service()
|
||||
service.update_all_groups()
|
||||
# Check member and blue user have groups updated
|
||||
self.assertTrue(mumble.update_groups.called)
|
||||
self.assertEqual(mumble.update_groups.call_count, 1)
|
||||
|
||||
def test_update_groups(self):
|
||||
# Check member has Member group updated
|
||||
service = self.service()
|
||||
member = User.objects.get(username=self.member)
|
||||
member.mumble.groups = '' # Remove the group set in setUp
|
||||
member.mumble.save()
|
||||
|
||||
service.update_groups(member)
|
||||
|
||||
mumble_user = MumbleUser.objects.get(user=member)
|
||||
self.assertIn(DEFAULT_AUTH_GROUP, mumble_user.groups)
|
||||
|
||||
# Check none user does not have groups updated
|
||||
service = self.service()
|
||||
none_user = User.objects.get(username=self.none_user)
|
||||
result = service.update_groups(none_user)
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_validate_user(self):
|
||||
service = self.service()
|
||||
# Test member is not deleted
|
||||
member = User.objects.get(username=self.member)
|
||||
service.validate_user(member)
|
||||
self.assertTrue(member.mumble)
|
||||
|
||||
# Test none user is deleted
|
||||
none_user = User.objects.get(username=self.none_user)
|
||||
MumbleUser.objects.create(user=none_user)
|
||||
service.validate_user(none_user)
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
_ = User.objects.get(username=self.none_user).mumble
|
||||
|
||||
def test_delete_user(self):
|
||||
member = User.objects.get(username=self.member)
|
||||
|
||||
service = self.service()
|
||||
result = service.delete_user(member)
|
||||
|
||||
self.assertTrue(result)
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
_ = User.objects.get(username=self.member).mumble
|
||||
|
||||
def test_render_services_ctrl(self):
|
||||
service = self.service()
|
||||
member = User.objects.get(username=self.member)
|
||||
request = RequestFactory().get('/services/')
|
||||
request.user = member
|
||||
|
||||
response = service.render_services_ctrl(request)
|
||||
self.assertTemplateUsed(service.service_ctrl_template)
|
||||
self.assertIn(urls.reverse('mumble:deactivate'), response)
|
||||
self.assertIn(urls.reverse('mumble:reset_password'), response)
|
||||
self.assertIn(urls.reverse('mumble:set_password'), response)
|
||||
|
||||
# Test register becomes available
|
||||
member.mumble.delete()
|
||||
member = User.objects.get(username=self.member)
|
||||
request.user = member
|
||||
response = service.render_services_ctrl(request)
|
||||
self.assertIn(urls.reverse('mumble:activate'), response)
|
||||
|
||||
|
||||
class MumbleViewsTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.member = AuthUtils.create_member('auth_member')
|
||||
self.member.email = 'auth_member@example.com'
|
||||
self.member.save()
|
||||
AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation', corp_ticker='TESTR')
|
||||
self.member = User.objects.get(pk=self.member.pk)
|
||||
add_permissions()
|
||||
|
||||
def login(self):
|
||||
self.client.force_login(self.member)
|
||||
|
||||
def test_activate_update(self):
|
||||
self.login()
|
||||
expected_username = 'auth_member'
|
||||
expected_displayname = '[TESTR]auth_member'
|
||||
response = self.client.get(urls.reverse('mumble:activate'), follow=False)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, expected_username)
|
||||
# create
|
||||
mumble_user = MumbleUser.objects.get(user=self.member)
|
||||
self.assertEqual(mumble_user.username, expected_username)
|
||||
self.assertTrue(MumbleUser.objects.user_exists(expected_username))
|
||||
self.assertEqual(str(mumble_user), expected_username)
|
||||
self.assertEqual(mumble_user.display_name, expected_displayname)
|
||||
self.assertTrue(mumble_user.pwhash)
|
||||
self.assertIn('Guest', mumble_user.groups)
|
||||
self.assertIn('Member', mumble_user.groups)
|
||||
self.assertIn(',', mumble_user.groups)
|
||||
# test update
|
||||
self.member.profile.main_character.character_name = "auth_member_updated"
|
||||
self.member.profile.main_character.corporation_ticker = "TESTU"
|
||||
self.member.profile.main_character.save()
|
||||
mumble_user.update_display_name()
|
||||
mumble_user = MumbleUser.objects.get(user=self.member)
|
||||
expected_displayname = '[TESTU]auth_member_updated'
|
||||
self.assertEqual(mumble_user.username, expected_username)
|
||||
self.assertTrue(MumbleUser.objects.user_exists(expected_username))
|
||||
self.assertEqual(str(mumble_user), expected_username)
|
||||
self.assertEqual(mumble_user.display_name, expected_displayname)
|
||||
self.assertTrue(mumble_user.pwhash)
|
||||
self.assertIn('Guest', mumble_user.groups)
|
||||
self.assertIn('Member', mumble_user.groups)
|
||||
self.assertIn(',', mumble_user.groups)
|
||||
|
||||
def test_deactivate_post(self):
|
||||
self.login()
|
||||
MumbleUser.objects.create(user=self.member)
|
||||
|
||||
response = self.client.post(urls.reverse('mumble:deactivate'))
|
||||
|
||||
self.assertRedirects(response, expected_url=urls.reverse('services:services'), target_status_code=200)
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
_ = User.objects.get(pk=self.member.pk).mumble
|
||||
|
||||
def test_set_password(self):
|
||||
self.login()
|
||||
created = MumbleUser.objects.create(user=self.member)
|
||||
old_pwd = created.credentials.get('password')
|
||||
|
||||
response = self.client.post(urls.reverse('mumble:set_password'), data={'password': '1234asdf'})
|
||||
|
||||
self.assertNotEqual(MumbleUser.objects.get(user=self.member).pwhash, old_pwd)
|
||||
self.assertRedirects(response, expected_url=urls.reverse('services:services'), target_status_code=200)
|
||||
|
||||
def test_reset_password(self):
|
||||
self.login()
|
||||
created = MumbleUser.objects.create(user=self.member)
|
||||
old_pwd = created.credentials.get('password')
|
||||
|
||||
response = self.client.get(urls.reverse('mumble:reset_password'))
|
||||
|
||||
self.assertNotEqual(MumbleUser.objects.get(user=self.member).pwhash, old_pwd)
|
||||
self.assertTemplateUsed(response, 'services/service_credentials.html')
|
||||
self.assertContains(response, 'auth_member')
|
||||
|
||||
class MumbleManagerTestCase(TestCase):
|
||||
def setUp(self):
|
||||
from .models import MumbleManager
|
||||
self.manager = MumbleManager
|
||||
|
||||
def test_generate_random_password(self):
|
||||
password = self.manager.generate_random_pass()
|
||||
|
||||
self.assertEqual(len(password), 16)
|
||||
self.assertIsInstance(password, str)
|
||||
|
||||
def test_gen_pwhash(self):
|
||||
pwhash = self.manager.gen_pwhash('test')
|
||||
|
||||
self.assertEqual(pwhash[:15], '$bcrypt-sha256$')
|
||||
self.assertEqual(len(pwhash), 83)
|
88
allianceauth/services/modules/mumble/tests/test_hooks.py
Normal file
88
allianceauth/services/modules/mumble/tests/test_hooks.py
Normal file
@ -0,0 +1,88 @@
|
||||
from allianceauth.services.modules.mumble.auth_hooks import MumbleService
|
||||
from allianceauth.services.modules.mumble.models import MumbleUser
|
||||
from allianceauth.services.modules.mumble.tasks import MumbleTasks
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django import urls
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
MODULE_PATH = 'allianceauth.services.modules.mumble'
|
||||
DEFAULT_AUTH_GROUP = 'Member'
|
||||
|
||||
|
||||
def add_permissions():
|
||||
permission = Permission.objects.get(codename='access_mumble')
|
||||
members = Group.objects.get_or_create(name=DEFAULT_AUTH_GROUP)[0]
|
||||
AuthUtils.add_permissions_to_groups([permission], [members])
|
||||
|
||||
|
||||
class MumbleHooksTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.member = 'member_user'
|
||||
member = AuthUtils.create_member(self.member)
|
||||
AuthUtils.add_main_character(member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation', corp_ticker='TESTR')
|
||||
member = User.objects.get(pk=member.pk)
|
||||
MumbleUser.objects.create(user=member)
|
||||
self.none_user = 'none_user'
|
||||
none_user = AuthUtils.create_user(self.none_user)
|
||||
self.service = MumbleService
|
||||
add_permissions()
|
||||
|
||||
def test_has_account(self):
|
||||
member = User.objects.get(username=self.member)
|
||||
none_user = User.objects.get(username=self.none_user)
|
||||
self.assertTrue(MumbleTasks.has_account(member))
|
||||
self.assertFalse(MumbleTasks.has_account(none_user))
|
||||
|
||||
def test_service_enabled(self):
|
||||
service = self.service()
|
||||
member = User.objects.get(username=self.member)
|
||||
none_user = User.objects.get(username=self.none_user)
|
||||
|
||||
self.assertTrue(service.service_active_for_user(member))
|
||||
self.assertFalse(service.service_active_for_user(none_user))
|
||||
|
||||
def test_validate_user(self):
|
||||
service = self.service()
|
||||
# Test member is not deleted
|
||||
member = User.objects.get(username=self.member)
|
||||
service.validate_user(member)
|
||||
self.assertTrue(member.mumble)
|
||||
|
||||
# Test none user is deleted
|
||||
none_user = User.objects.get(username=self.none_user)
|
||||
MumbleUser.objects.create(user=none_user)
|
||||
service.validate_user(none_user)
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
none_mumble = User.objects.get(username=self.none_user).mumble
|
||||
|
||||
def test_delete_user(self):
|
||||
member = User.objects.get(username=self.member)
|
||||
|
||||
service = self.service()
|
||||
result = service.delete_user(member)
|
||||
|
||||
self.assertTrue(result)
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
mumble_user = User.objects.get(username=self.member).mumble
|
||||
|
||||
def test_render_services_ctrl(self):
|
||||
service = self.service()
|
||||
member = User.objects.get(username=self.member)
|
||||
request = RequestFactory().get('/services/')
|
||||
request.user = member
|
||||
|
||||
response = service.render_services_ctrl(request)
|
||||
self.assertTemplateUsed(service.service_ctrl_template)
|
||||
self.assertIn(urls.reverse('mumble:deactivate'), response)
|
||||
self.assertIn(urls.reverse('mumble:reset_password'), response)
|
||||
self.assertIn(urls.reverse('mumble:set_password'), response)
|
||||
|
||||
# Test register becomes available
|
||||
member.mumble.delete()
|
||||
member = User.objects.get(username=self.member)
|
||||
request.user = member
|
||||
response = service.render_services_ctrl(request)
|
||||
self.assertIn(urls.reverse('mumble:activate'), response)
|
117
allianceauth/services/modules/mumble/tests/test_models.py
Normal file
117
allianceauth/services/modules/mumble/tests/test_models.py
Normal file
@ -0,0 +1,117 @@
|
||||
import unittest
|
||||
from allianceauth.authentication.admin import Permission
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import User, Group
|
||||
from unittest.mock import patch
|
||||
from datetime import timedelta
|
||||
|
||||
from ..models import (
|
||||
MumbleUser,
|
||||
TempLink,
|
||||
TempUser,
|
||||
IdlerHandler,
|
||||
MumbleServerServer
|
||||
)
|
||||
|
||||
MODULE_PATH = 'allianceauth.services.modules.mumble'
|
||||
DEFAULT_AUTH_GROUP = 'Member'
|
||||
|
||||
|
||||
def add_permissions():
|
||||
permission = Permission.objects.get(codename='access_mumble')
|
||||
members = Group.objects.get_or_create(name=DEFAULT_AUTH_GROUP)[0]
|
||||
AuthUtils.add_permissions_to_groups([permission], [members])
|
||||
|
||||
|
||||
class MumbleUserTests(TestCase):
|
||||
def setUp(self):
|
||||
self.member = AuthUtils.create_member('auth_member')
|
||||
self.member.email = 'auth_member@example.com'
|
||||
self.member.save()
|
||||
AuthUtils.add_main_character(self.member, 'john_mumble', '12345', corp_id='111', corp_name='Test Corporation', corp_ticker='TESTR')
|
||||
self.member = User.objects.get(pk=self.member.pk)
|
||||
add_permissions()
|
||||
self.mumble_user = MumbleUser.objects.create(user=self.member)
|
||||
|
||||
def test_mumble_user_str(self):
|
||||
"""
|
||||
Test that __str__ returns the username.
|
||||
"""
|
||||
self.assertEqual(str(self.mumble_user), 'john_mumble')
|
||||
|
||||
def test_update_password_no_arg(self):
|
||||
"""
|
||||
Test update_password when no password is provided
|
||||
(it should generate a random one).
|
||||
"""
|
||||
old_pwhash = self.mumble_user.pwhash
|
||||
self.mumble_user.update_password() # No password argument
|
||||
# pwhash should have changed (random pass)
|
||||
self.assertNotEqual(old_pwhash, self.mumble_user.pwhash)
|
||||
self.assertTrue(self.mumble_user.credentials) # Should have 'username' & 'password'
|
||||
|
||||
def test_reset_password(self):
|
||||
"""
|
||||
reset_password is basically an alias to update_password with no password argument.
|
||||
"""
|
||||
old_pwhash = self.mumble_user.pwhash
|
||||
self.mumble_user.reset_password()
|
||||
self.assertNotEqual(old_pwhash, self.mumble_user.pwhash)
|
||||
self.assertTrue(self.mumble_user.credentials)
|
||||
|
||||
|
||||
class IdlerHandlerTests(TestCase):
|
||||
def setUp(self):
|
||||
self.idler = IdlerHandler.objects.create(
|
||||
name="MyAFKIdler",
|
||||
enabled=True,
|
||||
seconds=7200,
|
||||
interval=120,
|
||||
channel=999,
|
||||
denylist=False,
|
||||
list="some_list"
|
||||
)
|
||||
|
||||
def test_idler_handler_str(self):
|
||||
self.assertEqual(str(self.idler), "MyAFKIdler")
|
||||
|
||||
|
||||
class MumbleServerServerTests(TestCase):
|
||||
def setUp(self):
|
||||
self.idler = IdlerHandler.objects.create(
|
||||
name="MyAFKIdler",
|
||||
enabled=True,
|
||||
seconds=3600,
|
||||
interval=60,
|
||||
channel=999,
|
||||
denylist=True,
|
||||
list=""
|
||||
)
|
||||
self.server = MumbleServerServer.objects.create(
|
||||
name="MyMumbleServer",
|
||||
ip="127.0.0.1",
|
||||
endpoint="127.0.0.1",
|
||||
port=6502,
|
||||
secret="supersecret",
|
||||
watchdog=30,
|
||||
slice="MumbleServer.ice",
|
||||
virtual_servers="1,2",
|
||||
avatar_enable=True,
|
||||
reject_on_error=True,
|
||||
offset=1000000000,
|
||||
idler_handler=self.idler
|
||||
)
|
||||
|
||||
def test_mumble_server_str(self):
|
||||
"""
|
||||
Test string representation of MumbleServerServer.
|
||||
"""
|
||||
self.assertEqual(str(self.server), "MyMumbleServer")
|
||||
|
||||
def test_virtual_servers_list(self):
|
||||
"""
|
||||
The virtual_servers_list should parse '1,2' into [1, 2].
|
||||
"""
|
||||
self.assertEqual(self.server.virtual_servers_list(), [1, 2])
|
93
allianceauth/services/modules/mumble/tests/test_views.py
Normal file
93
allianceauth/services/modules/mumble/tests/test_views.py
Normal file
@ -0,0 +1,93 @@
|
||||
from unittest import mock
|
||||
|
||||
from allianceauth.services.modules.mumble.auth_hooks import MumbleService
|
||||
from allianceauth.services.modules.mumble.models import MumbleUser
|
||||
from allianceauth.services.modules.mumble.tasks import MumbleTasks
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django import urls
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
MODULE_PATH = 'allianceauth.services.modules.mumble'
|
||||
DEFAULT_AUTH_GROUP = 'Member'
|
||||
|
||||
def add_permissions():
|
||||
permission = Permission.objects.get(codename='access_mumble')
|
||||
members = Group.objects.get_or_create(name=DEFAULT_AUTH_GROUP)[0]
|
||||
AuthUtils.add_permissions_to_groups([permission], [members])
|
||||
|
||||
class MumbleViewsTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.member = AuthUtils.create_member('auth_member')
|
||||
self.member.email = 'auth_member@example.com'
|
||||
self.member.save()
|
||||
AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation', corp_ticker='TESTR')
|
||||
self.member = User.objects.get(pk=self.member.pk)
|
||||
add_permissions()
|
||||
|
||||
def login(self):
|
||||
self.client.force_login(self.member)
|
||||
|
||||
def test_activate_update(self):
|
||||
self.login()
|
||||
expected_username = 'auth_member'
|
||||
expected_displayname = '[TESTR]auth_member'
|
||||
response = self.client.get(urls.reverse('mumble:activate'), follow=False)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, expected_username)
|
||||
# create
|
||||
mumble_user = MumbleUser.objects.get(user=self.member)
|
||||
self.assertEqual(mumble_user.username, expected_username)
|
||||
self.assertEqual(str(mumble_user), expected_username)
|
||||
self.assertEqual(mumble_user.display_name, expected_displayname)
|
||||
self.assertTrue(mumble_user.pwhash)
|
||||
self.assertIn('Guest', mumble_user.groups)
|
||||
self.assertIn('Member', mumble_user.groups)
|
||||
self.assertIn(',', mumble_user.groups)
|
||||
# test update
|
||||
self.member.profile.main_character.character_name = "auth_member_updated"
|
||||
self.member.profile.main_character.corporation_ticker = "TESTU"
|
||||
self.member.profile.main_character.save()
|
||||
mumble_user = MumbleUser.objects.get(user=self.member)
|
||||
expected_displayname = '[TESTU]auth_member_updated'
|
||||
self.assertEqual(mumble_user.username, expected_username)
|
||||
self.assertEqual(str(mumble_user), expected_username)
|
||||
self.assertEqual(mumble_user.display_name, expected_displayname)
|
||||
self.assertTrue(mumble_user.pwhash)
|
||||
self.assertIn('Guest', mumble_user.groups)
|
||||
self.assertIn('Member', mumble_user.groups)
|
||||
self.assertIn(',', mumble_user.groups)
|
||||
|
||||
|
||||
def test_deactivate_post(self):
|
||||
self.login()
|
||||
MumbleUser.objects.create(user=self.member)
|
||||
|
||||
response = self.client.post(urls.reverse('mumble:deactivate'))
|
||||
|
||||
self.assertRedirects(response, expected_url=urls.reverse('services:services'), target_status_code=200)
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
mumble_user = User.objects.get(pk=self.member.pk).mumble
|
||||
|
||||
def test_set_password(self):
|
||||
self.login()
|
||||
created = MumbleUser.objects.create(user=self.member)
|
||||
old_pwd = created.credentials.get('password')
|
||||
|
||||
response = self.client.post(urls.reverse('mumble:set_password'), data={'password': '1234asdf'})
|
||||
|
||||
self.assertNotEqual(MumbleUser.objects.get(user=self.member).pwhash, old_pwd)
|
||||
self.assertRedirects(response, expected_url=urls.reverse('services:services'), target_status_code=200)
|
||||
|
||||
def test_reset_password(self):
|
||||
self.login()
|
||||
created = MumbleUser.objects.create(user=self.member)
|
||||
old_pwd = created.credentials.get('password')
|
||||
|
||||
response = self.client.get(urls.reverse('mumble:reset_password'))
|
||||
|
||||
self.assertNotEqual(MumbleUser.objects.get(user=self.member).pwhash, old_pwd)
|
||||
self.assertTemplateUsed(response, 'services/service_credentials.html')
|
||||
self.assertContains(response, 'auth_member')
|
@ -1,4 +1,4 @@
|
||||
from django.urls import include, path
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from . import views
|
||||
|
||||
@ -14,6 +14,10 @@ module_urls = [
|
||||
path('ajax/connection_history_data', views.connection_history_data, name="connection_history_data"),
|
||||
path('ajax/release_counts_data', views.release_counts_data, name="release_counts_data"),
|
||||
path('ajax/release_pie_chart_data', views.release_pie_chart_data, name="release_pie_chart_data"),
|
||||
# Temp Links
|
||||
path("templinks/", views.templinks, name="templinks"),
|
||||
re_path(r"^join/(?P<link_ref>[\w\-]+)/$", views.link, name="join"),
|
||||
re_path(r"^nuke/(?P<link_ref>[\w\-]+)/$", views.nuke, name="nuke"),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -1,19 +1,29 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import logging
|
||||
|
||||
from allianceauth.authentication.models import get_guest_state
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from allianceauth.services.forms import ServicePasswordModelForm
|
||||
from allianceauth.services.abstract import BaseCreatePasswordServiceAccountView, BaseDeactivateServiceAccountView, \
|
||||
BaseResetPasswordServiceAccountView, BaseSetPasswordServiceAccountView
|
||||
from allianceauth.services.modules.mumble import app_settings
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||
from django.db.models import Count
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.http import Http404, HttpResponse, JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.crypto import get_random_string
|
||||
from esi.views import sso_redirect
|
||||
|
||||
from .models import MumbleUser, TempLink, TempUser
|
||||
from allianceauth.services.abstract import (
|
||||
BaseCreatePasswordServiceAccountView,
|
||||
BaseDeactivateServiceAccountView,
|
||||
BaseResetPasswordServiceAccountView,
|
||||
BaseSetPasswordServiceAccountView,
|
||||
)
|
||||
from allianceauth.services.forms import ServicePasswordModelForm
|
||||
|
||||
from .models import MumbleUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -61,15 +71,17 @@ def connection_history(request) -> HttpResponse:
|
||||
@login_required
|
||||
@permission_required("mumble.view_connection_history")
|
||||
def connection_history_data(request) -> JsonResponse:
|
||||
connection_history_data = MumbleUser.objects.all(
|
||||
).values(
|
||||
'user',
|
||||
'display_name',
|
||||
'release',
|
||||
'version',
|
||||
'last_connect',
|
||||
'last_disconnect',
|
||||
)
|
||||
users = MumbleUser.objects.all()
|
||||
connection_history_data = []
|
||||
for user in users:
|
||||
connection_history_data.append({
|
||||
'user': str(user),
|
||||
'display_name': user.display_name,
|
||||
'release': user.release,
|
||||
'version': user.version,
|
||||
'last_connect': user.last_connect,
|
||||
'last_disconnect': user.last_disconnect,
|
||||
})
|
||||
|
||||
return JsonResponse({"connection_history_data": list(connection_history_data)})
|
||||
|
||||
@ -93,3 +105,170 @@ def release_pie_chart_data(request) -> JsonResponse:
|
||||
"labels": list(release_counts.values_list("release", flat=True)),
|
||||
"values": list(release_counts.values_list("user_count", flat=True)),
|
||||
})
|
||||
|
||||
|
||||
class PseudoProfile:
|
||||
def __init__(self, main):
|
||||
self.main_character = main
|
||||
self.state = get_guest_state()
|
||||
|
||||
|
||||
class PseudoUser:
|
||||
def __init__(self, main, username):
|
||||
self.username = username
|
||||
self.profile = PseudoProfile(main)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("mumble.create_new_links")
|
||||
def templinks(request) -> HttpResponse:
|
||||
tl = None
|
||||
|
||||
if request.method == "POST":
|
||||
duration = request.POST.get("time")
|
||||
|
||||
if duration in ["3", "6", "12", "24"]:
|
||||
tl = TempLink.objects.create(
|
||||
creator=request.user.profile.main_character,
|
||||
link_ref=get_random_string(15),
|
||||
expires=datetime.now(timezone.utc) + timedelta(hours=int(duration))
|
||||
)
|
||||
tl.save()
|
||||
|
||||
tl_list = TempLink.objects.prefetch_related("creator").filter(expires__gte=datetime.now(timezone.utc))
|
||||
ex_tl_list = TempLink.objects.prefetch_related("creator").filter(expires__lt=datetime.now(timezone.utc))
|
||||
|
||||
context = {
|
||||
"tl": tl,
|
||||
"text": "Make Links",
|
||||
"tl_list": tl_list,
|
||||
"ex_tl_list": ex_tl_list,
|
||||
}
|
||||
|
||||
return render(
|
||||
request=request, template_name="services/mumble/index.html", context=context
|
||||
)
|
||||
|
||||
|
||||
def link(request, link_ref):
|
||||
try:
|
||||
templink = TempLink.objects.get(link_ref=link_ref)
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404("Temp Link Does not Exist")
|
||||
|
||||
token = _check_callback(request=request)
|
||||
if token:
|
||||
return link_sso(request=request, token=token, link=templink)
|
||||
|
||||
if app_settings.MUMBLE_TEMPS_FORCE_SSO: # default always SSO
|
||||
# prompt the user to log in for a new token
|
||||
return sso_redirect(request=request, scopes=["publicData"])
|
||||
|
||||
if request.method == "POST": # ok so maybe we want to let some other people in too.
|
||||
if request.POST.get("sso", False) == "False": # they picked user
|
||||
name = request.POST.get("name", False)
|
||||
association = request.POST.get("association", False)
|
||||
|
||||
return link_username(
|
||||
request=request, name=name, association=association, link=templink
|
||||
)
|
||||
elif request.POST.get("sso", False) == "True": # they picked SSO
|
||||
# prompt the user to log in for a new token
|
||||
return sso_redirect(request=request, scopes=["publicData"])
|
||||
|
||||
context = {"link": templink}
|
||||
|
||||
return render(
|
||||
request=request, template_name="services/mumble/login.html", context=context
|
||||
)
|
||||
|
||||
|
||||
def link_username(request, name, association, link):
|
||||
username = get_random_string(length=10)
|
||||
|
||||
while TempUser.objects.filter(username=username).exists(): # force unique
|
||||
username = get_random_string(length=10)
|
||||
|
||||
password = get_random_string(length=15)
|
||||
|
||||
display_name = "{}[{}] {}".format(
|
||||
app_settings.MUMBLE_TEMPS_LOGIN_PREFIX, association, name
|
||||
)
|
||||
|
||||
temp_user = TempUser.objects.create(
|
||||
username=username,
|
||||
password=password,
|
||||
name=display_name,
|
||||
expires=link.expires,
|
||||
templink=link,
|
||||
)
|
||||
|
||||
connect_url = f"{username}:{password}@{settings.MUMBLE_URL}"
|
||||
|
||||
context = {
|
||||
"temp_user": temp_user,
|
||||
"link": link,
|
||||
"connect_url": connect_url,
|
||||
"mumble": settings.MUMBLE_URL,
|
||||
}
|
||||
|
||||
return render(
|
||||
request=request, template_name="services/mumble/link.html", context=context
|
||||
)
|
||||
|
||||
|
||||
def link_sso(request, token, link):
|
||||
try:
|
||||
char = EveCharacter.objects.get(character_id=token.character_id)
|
||||
except ObjectDoesNotExist:
|
||||
try: # create a new character, we should not get here.
|
||||
char = EveCharacter.objects.update_character(
|
||||
character_id=token.character_id
|
||||
)
|
||||
except: # noqa: E722
|
||||
pass # Yeah… ain't gonna happen
|
||||
except MultipleObjectsReturned:
|
||||
pass # authenticator won't care…, but the DB will be unhappy.
|
||||
|
||||
username = get_random_string(length=10)
|
||||
|
||||
while TempUser.objects.filter(username=username).exists(): # force unique
|
||||
username = get_random_string(length=10)
|
||||
|
||||
password = get_random_string(length=15)
|
||||
|
||||
temp_user = TempUser.objects.create(
|
||||
username=username,
|
||||
password=password,
|
||||
expires=link.expires,
|
||||
templink=link,
|
||||
character_id=char.character_id,
|
||||
)
|
||||
|
||||
connect_url = f"{username}:{password}@{settings.MUMBLE_URL}"
|
||||
|
||||
context = {
|
||||
"temp_user": temp_user,
|
||||
"link": link,
|
||||
"connect_url": connect_url,
|
||||
"mumble": settings.MUMBLE_URL,
|
||||
}
|
||||
|
||||
return render(
|
||||
request=request, template_name="services/mumble/link.html", context=context
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("mumble.create_new_links")
|
||||
def nuke(request, link_ref):
|
||||
try:
|
||||
TempLink.objects.get(link_ref=link_ref).delete()
|
||||
TempUser.objects.filter(templink__isnull=True).delete()
|
||||
|
||||
messages.success(request=request, message=f"Deleted Token {link_ref}")
|
||||
except: # noqa: E722
|
||||
messages.error(request=request, message=f"Deleted Token {link_ref}")
|
||||
pass # Crappy link
|
||||
|
||||
return redirect(to="mumble:index")
|
||||
|
@ -6,12 +6,21 @@ Mumble is a free voice chat server. While not as flashy as TeamSpeak, it has all
|
||||
|
||||
## Configuring Auth
|
||||
|
||||
In your auth project's settings file (`aa-docker/conf/local.py`), do the following:
|
||||
In your auth project's settings file (`myauth/settings/local.py`), do the following:
|
||||
|
||||
- Add `'allianceauth.services.modules.mumble',` to `INSTALLED_APPS` in your `local.py`
|
||||
- Append the following to your auth project's settings file:
|
||||
- Add `'allianceauth.services.modules.mumble',` to your `INSTALLED_APPS` list
|
||||
- Set `MUMBLE_URL` to the public address of your mumble server. Do not include any leading `http://` or `mumble://`.
|
||||
|
||||
Example config:
|
||||
|
||||
```python
|
||||
# Installed apps
|
||||
INSTALLED_APPS += [
|
||||
# ...
|
||||
'allianceauth.services.modules.mumble'
|
||||
# ...
|
||||
]
|
||||
|
||||
# Mumble Configuration
|
||||
MUMBLE_URL = "mumble.example.com"
|
||||
```
|
||||
@ -33,16 +42,29 @@ docker compose exec allianceauth_gunicorn bash
|
||||
auth migrate
|
||||
```
|
||||
|
||||
## Configuring Authenticator
|
||||
|
||||
The Authenticator is configured via Django Admin, visit `/admin/mumble/mumbleserverserver/` in
|
||||
|
||||
Name: TEST
|
||||
Host IP Address: 127.0.0.1
|
||||
Endpoint IP Address: 127.0.0.1
|
||||
Port: 6502
|
||||
ICE Secret: ICESECRETWRITE
|
||||
Watchdog Interval: 30
|
||||
Slice: MumbleServer.ice (Mumble >=1.5.17)
|
||||
Virtual Servers: 1
|
||||
|
||||
Enable EVE Avatars
|
||||
|
||||
Reject Unauthenticated
|
||||
ID Offset: 1000000000
|
||||
Idler Handler:
|
||||
|
||||
## Docker Installations
|
||||
|
||||
### Installing Mumble and Authenticator
|
||||
|
||||
Inside your `aa-docker` directory, clone the authenticator to a sub directory as follows
|
||||
|
||||
```shell
|
||||
git clone https://gitlab.com/allianceauth/mumble-authenticator.git
|
||||
```
|
||||
|
||||
Add the following to your `docker-compose.yml` under the `services:` section
|
||||
|
||||
```docker
|
||||
@ -66,27 +88,16 @@ Add the following to your `docker-compose.yml` under the `services:` section
|
||||
max-size: "10Mb"
|
||||
max-file: "5"
|
||||
|
||||
mumble-authenticator:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./mumble-authenticator/Dockerfile
|
||||
restart: always
|
||||
volumes:
|
||||
- ./mumble-authenticator/authenticator.py:/authenticator.py
|
||||
- ./mumble-authenticator/authenticator.ini.docker:/authenticator.ini
|
||||
environment:
|
||||
- MUMBLE_SUPERUSER_PASSWORD=${MUMBLE_SUPERUSER_PASSWORD}
|
||||
- MUMBLE_CONFIG_ice="tcp -h 127.0.0.1 -p 6502"
|
||||
- MUMBLE_CONFIG_icesecretwrite=${MUMBLE_ICESECRETWRITE}
|
||||
- MUMBLE_CONFIG_serverpassword=${MUMBLE_SERVERPASSWORD}
|
||||
depends_on:
|
||||
- mumble-server
|
||||
- auth_mysql
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10Mb"
|
||||
max-file: "5"
|
||||
allianceauth_mumble_authenticator:
|
||||
container_name: allianceauth_mumble_authenticator
|
||||
<<: [*allianceauth-base]
|
||||
entrypoint: [
|
||||
"python",
|
||||
"manage.py",
|
||||
"mumble_authenticator",
|
||||
"--server_id=1"
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
@ -49,26 +49,6 @@ sudo yum install mumble-server
|
||||
:::
|
||||
::::
|
||||
|
||||
### Installing Mumble Authenticator
|
||||
|
||||
Next, we need to download the latest authenticator release from the [authenticator repository](https://gitlab.com/allianceauth/mumble-authenticator).
|
||||
|
||||
```shell
|
||||
git clone https://gitlab.com/allianceauth/mumble-authenticator /home/allianceserver/mumble-authenticator
|
||||
```
|
||||
|
||||
We will now install the authenticator into your Auth virtual environment. Please make sure to activate it first:
|
||||
|
||||
```shell
|
||||
source /home/allianceserver/venv/auth/bin/activate
|
||||
```
|
||||
|
||||
Install the python dependencies for the mumble authenticator. Note that this process can take 2 to 10 minutes to complete.
|
||||
|
||||
```shell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Configuring Mumble Server
|
||||
|
||||
Mumble ships with a configuration file that needs customization. By default, it's located at `/etc/mumble-server.ini`. Open it with your favorite text editor:
|
||||
@ -102,66 +82,6 @@ sudo service mumble-server restart
|
||||
|
||||
That's it! Your server is ready to be connected to at example.com:64738
|
||||
|
||||
## Configuring Mumble Authenticator
|
||||
|
||||
The ICE authenticator lives in the mumble-authenticator repository, cd to the directory where you cloned it.
|
||||
|
||||
Make a copy of the default config:
|
||||
|
||||
```shell
|
||||
cp authenticator.ini.example authenticator.ini
|
||||
```
|
||||
|
||||
Edit `authenticator.ini` and change these values:
|
||||
|
||||
- `[database]`
|
||||
- `user =` your allianceserver MySQL user
|
||||
- `password =` your allianceserver MySQL user's password
|
||||
- `[ice]`
|
||||
- `secret =` the `icewritesecret` password set earlier
|
||||
|
||||
Test your configuration by starting it:
|
||||
|
||||
```shell
|
||||
python /home/allianceserver/mumble-authenticator/authenticator.py
|
||||
```
|
||||
|
||||
And finally, ensure the allianceserver user has read/write permissions to the mumble authenticator files before proceeding:
|
||||
|
||||
```shell
|
||||
sudo chown -R allianceserver:allianceserver /home/allianceserver/mumble-authenticator
|
||||
```
|
||||
|
||||
The authenticator needs to be running 24/7 to validate users on Mumble. This can be achieved by adding a section to your auth project's supervisor config file like the following example:
|
||||
|
||||
```ini
|
||||
[program:authenticator]
|
||||
command=/home/allianceserver/venv/auth/bin/python authenticator.py
|
||||
directory=/home/allianceserver/mumble-authenticator
|
||||
user=allianceserver
|
||||
stdout_logfile=/home/allianceserver/myauth/log/authenticator.log
|
||||
stderr_logfile=/home/allianceserver/myauth/log/authenticator.log
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
priority=996
|
||||
```
|
||||
|
||||
In addition, we'd recommend adding the authenticator to Auth's restart group in your supervisor conf. For that, you need to add it to the group line as shown in the following example:
|
||||
|
||||
```ini
|
||||
[group:myauth]
|
||||
programs=beat,worker,gunicorn,authenticator
|
||||
priority=999
|
||||
```
|
||||
|
||||
To enable the changes in your supervisor configuration, you need to restart the supervisor process itself. And before we do that, we are shutting down the current Auth supervisors gracefully:
|
||||
|
||||
```shell
|
||||
sudo supervisor stop myauth:
|
||||
sudo systemctl restart supervisor
|
||||
```
|
||||
|
||||
## Configuring Auth
|
||||
|
||||
In your auth project's settings file (`myauth/settings/local.py`), do the following:
|
||||
@ -187,10 +107,57 @@ Finally, run migrations and restart your supervisor to complete the setup:
|
||||
|
||||
```shell
|
||||
python /home/allianceserver/myauth/manage.py migrate
|
||||
supervisorctl restart myauth:
|
||||
```
|
||||
|
||||
## Configuring Authenticator
|
||||
|
||||
The Authenticator is configured via Django Admin, visit `/admin/mumble/mumbleserverserver/` in
|
||||
|
||||
Name: TEST
|
||||
Host IP Address: 127.0.0.1
|
||||
Endpoint IP Address: 127.0.0.1
|
||||
Port: 6502
|
||||
ICE Secret: ICESECRETWRITE
|
||||
Watchdog Interval: 30
|
||||
Slice: MumbleServer.ice (Mumble >=1.5.17)
|
||||
Virtual Servers: 1
|
||||
|
||||
Enable EVE Avatars
|
||||
|
||||
Reject Unauthenticated
|
||||
ID Offset: 1000000000
|
||||
Idler Handler:
|
||||
|
||||
## Running Authenticator
|
||||
|
||||
The authenticator needs to be running 24/7 to validate users on Mumble. This can be achieved by adding a section to your auth project's supervisor config file like the following example:
|
||||
|
||||
```ini
|
||||
[program:authenticator]
|
||||
command=/home/allianceserver/venv/auth/bin/python manage.py mumble_authenticator
|
||||
directory=/home/allianceserver/myauth/
|
||||
stdout_logfile=/home/allianceserver/myauth/log/authenticator.log
|
||||
stderr_logfile=/home/allianceserver/myauth/log/authenticator.log
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
priority=996
|
||||
```
|
||||
|
||||
In addition, we'd recommend adding the authenticator to Auth's restart group in your supervisor conf. For that, you need to add it to the group line as shown in the following example:
|
||||
|
||||
```ini
|
||||
[group:myauth]
|
||||
programs=beat,worker,gunicorn,authenticator
|
||||
priority=999
|
||||
```
|
||||
|
||||
To enable the changes in your supervisor configuration, you need to restart the supervisor process itself. And before we do that, we are shutting down the current Auth supervisors gracefully:
|
||||
|
||||
```shell
|
||||
supervisorctl restart myauth:
|
||||
sudo supervisor stop myauth:
|
||||
sudo systemctl restart supervisor
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
Loading…
x
Reference in New Issue
Block a user