Преглед изворни кода

Implement custom IPublicKeyManager provider

tags/v1.7.0
James Moger пре 10 година
родитељ
комит
241f573656

+ 189
- 189
src/main/java/com/gitblit/FederationClient.java Прегледај датотеку

@@ -1,189 +1,189 @@
/*
* Copyright 2011 gitblit.com.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.gitblit;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import com.gitblit.manager.FederationManager;
import com.gitblit.manager.GitblitManager;
import com.gitblit.manager.IGitblit;
import com.gitblit.manager.INotificationManager;
import com.gitblit.manager.RepositoryManager;
import com.gitblit.manager.RuntimeManager;
import com.gitblit.manager.UserManager;
import com.gitblit.models.FederationModel;
import com.gitblit.models.Mailing;
import com.gitblit.service.FederationPullService;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.StringUtils;
/**
* Command-line client to pull federated Gitblit repositories.
*
* @author James Moger
*
*/
public class FederationClient {
public static void main(String[] args) {
Params params = new Params();
CmdLineParser parser = new CmdLineParser(params);
try {
parser.parseArgument(args);
} catch (CmdLineException t) {
usage(parser, t);
}
System.out.println("Gitblit Federation Client v" + Constants.getVersion() + " (" + Constants.getBuildDate() + ")");
// command-line specified base folder
File baseFolder = new File(System.getProperty("user.dir"));
if (!StringUtils.isEmpty(params.baseFolder)) {
baseFolder = new File(params.baseFolder);
}
File regFile = com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$, baseFolder, params.registrationsFile);
FileSettings settings = new FileSettings(regFile.getAbsolutePath());
List<FederationModel> registrations = new ArrayList<FederationModel>();
if (StringUtils.isEmpty(params.url)) {
registrations.addAll(FederationUtils.getFederationRegistrations(settings));
} else {
if (StringUtils.isEmpty(params.token)) {
System.out.println("Must specify --token parameter!");
System.exit(0);
}
FederationModel model = new FederationModel("Gitblit");
model.url = params.url;
model.token = params.token;
model.mirror = params.mirror;
model.bare = params.bare;
model.folder = "";
registrations.add(model);
}
if (registrations.size() == 0) {
System.out.println("No Federation Registrations! Nothing to do.");
System.exit(0);
}
// command-line specified repositories folder
if (!StringUtils.isEmpty(params.repositoriesFolder)) {
settings.overrideSetting(Keys.git.repositoriesFolder, new File(
params.repositoriesFolder).getAbsolutePath());
}
// configure the Gitblit singleton for minimal, non-server operation
RuntimeManager runtime = new RuntimeManager(settings, baseFolder).start();
NoopNotificationManager notifications = new NoopNotificationManager().start();
UserManager users = new UserManager(runtime, null).start();
RepositoryManager repositories = new RepositoryManager(runtime, null, users).start();
FederationManager federation = new FederationManager(runtime, notifications, repositories).start();
IGitblit gitblit = new GitblitManager(runtime, null, notifications, users, null, null, repositories, null, federation);
FederationPullService puller = new FederationPullService(gitblit, federation.getFederationRegistrations()) {
@Override
public void reschedule(FederationModel registration) {
// NOOP
}
};
puller.run();
System.out.println("Finished.");
System.exit(0);
}
private static void usage(CmdLineParser parser, CmdLineException t) {
System.out.println(Constants.getGitBlitVersion());
System.out.println();
if (t != null) {
System.out.println(t.getMessage());
System.out.println();
}
if (parser != null) {
parser.printUsage(System.out);
}
System.exit(0);
}
/**
* Parameters class for FederationClient.
*/
private static class Params {
@Option(name = "--registrations", usage = "Gitblit Federation Registrations File", metaVar = "FILE")
public String registrationsFile = "${baseFolder}/federation.properties";
@Option(name = "--url", usage = "URL of Gitblit instance to mirror from", metaVar = "URL")
public String url;
@Option(name = "--mirror", usage = "Mirror repositories")
public boolean mirror;
@Option(name = "--bare", usage = "Create bare repositories")
public boolean bare;
@Option(name = "--token", usage = "Federation Token", metaVar = "TOKEN")
public String token;
@Option(name = "--baseFolder", usage = "Base folder for received data", metaVar = "PATH")
public String baseFolder;
@Option(name = "--repositoriesFolder", usage = "Destination folder for cloned repositories", metaVar = "PATH")
public String repositoriesFolder;
}
private static class NoopNotificationManager implements INotificationManager {
@Override
public NoopNotificationManager start() {
return this;
}
@Override
public NoopNotificationManager stop() {
return this;
}
@Override
public boolean isSendingMail() {
return false;
}
@Override
public void sendMailToAdministrators(String subject, String message) {
}
@Override
public void sendMail(String subject, String message, Collection<String> toAddresses) {
}
@Override
public void sendHtmlMail(String subject, String message, Collection<String> toAddresses) {
}
@Override
public void send(Mailing mailing) {
}
}
}
/*
* Copyright 2011 gitblit.com.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.gitblit;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import com.gitblit.manager.FederationManager;
import com.gitblit.manager.GitblitManager;
import com.gitblit.manager.IGitblit;
import com.gitblit.manager.INotificationManager;
import com.gitblit.manager.RepositoryManager;
import com.gitblit.manager.RuntimeManager;
import com.gitblit.manager.UserManager;
import com.gitblit.models.FederationModel;
import com.gitblit.models.Mailing;
import com.gitblit.service.FederationPullService;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.StringUtils;
/**
* Command-line client to pull federated Gitblit repositories.
*
* @author James Moger
*
*/
public class FederationClient {
public static void main(String[] args) {
Params params = new Params();
CmdLineParser parser = new CmdLineParser(params);
try {
parser.parseArgument(args);
} catch (CmdLineException t) {
usage(parser, t);
}
System.out.println("Gitblit Federation Client v" + Constants.getVersion() + " (" + Constants.getBuildDate() + ")");
// command-line specified base folder
File baseFolder = new File(System.getProperty("user.dir"));
if (!StringUtils.isEmpty(params.baseFolder)) {
baseFolder = new File(params.baseFolder);
}
File regFile = com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$, baseFolder, params.registrationsFile);
FileSettings settings = new FileSettings(regFile.getAbsolutePath());
List<FederationModel> registrations = new ArrayList<FederationModel>();
if (StringUtils.isEmpty(params.url)) {
registrations.addAll(FederationUtils.getFederationRegistrations(settings));
} else {
if (StringUtils.isEmpty(params.token)) {
System.out.println("Must specify --token parameter!");
System.exit(0);
}
FederationModel model = new FederationModel("Gitblit");
model.url = params.url;
model.token = params.token;
model.mirror = params.mirror;
model.bare = params.bare;
model.folder = "";
registrations.add(model);
}
if (registrations.size() == 0) {
System.out.println("No Federation Registrations! Nothing to do.");
System.exit(0);
}
// command-line specified repositories folder
if (!StringUtils.isEmpty(params.repositoriesFolder)) {
settings.overrideSetting(Keys.git.repositoriesFolder, new File(
params.repositoriesFolder).getAbsolutePath());
}
// configure the Gitblit singleton for minimal, non-server operation
RuntimeManager runtime = new RuntimeManager(settings, baseFolder).start();
NoopNotificationManager notifications = new NoopNotificationManager().start();
UserManager users = new UserManager(runtime, null).start();
RepositoryManager repositories = new RepositoryManager(runtime, null, users).start();
FederationManager federation = new FederationManager(runtime, notifications, repositories).start();
IGitblit gitblit = new GitblitManager(null, runtime, null, notifications, users, null, repositories, null, federation);
FederationPullService puller = new FederationPullService(gitblit, federation.getFederationRegistrations()) {
@Override
public void reschedule(FederationModel registration) {
// NOOP
}
};
puller.run();
System.out.println("Finished.");
System.exit(0);
}
private static void usage(CmdLineParser parser, CmdLineException t) {
System.out.println(Constants.getGitBlitVersion());
System.out.println();
if (t != null) {
System.out.println(t.getMessage());
System.out.println();
}
if (parser != null) {
parser.printUsage(System.out);
}
System.exit(0);
}
/**
* Parameters class for FederationClient.
*/
private static class Params {
@Option(name = "--registrations", usage = "Gitblit Federation Registrations File", metaVar = "FILE")
public String registrationsFile = "${baseFolder}/federation.properties";
@Option(name = "--url", usage = "URL of Gitblit instance to mirror from", metaVar = "URL")
public String url;
@Option(name = "--mirror", usage = "Mirror repositories")
public boolean mirror;
@Option(name = "--bare", usage = "Create bare repositories")
public boolean bare;
@Option(name = "--token", usage = "Federation Token", metaVar = "TOKEN")
public String token;
@Option(name = "--baseFolder", usage = "Base folder for received data", metaVar = "PATH")
public String baseFolder;
@Option(name = "--repositoriesFolder", usage = "Destination folder for cloned repositories", metaVar = "PATH")
public String repositoriesFolder;
}
private static class NoopNotificationManager implements INotificationManager {
@Override
public NoopNotificationManager start() {
return this;
}
@Override
public NoopNotificationManager stop() {
return this;
}
@Override
public boolean isSendingMail() {
return false;
}
@Override
public void sendMailToAdministrators(String subject, String message) {
}
@Override
public void sendMail(String subject, String message, Collection<String> toAddresses) {
}
@Override
public void sendHtmlMail(String subject, String message, Collection<String> toAddresses) {
}
@Override
public void send(Mailing mailing) {
}
}
}

+ 5
- 21
src/main/java/com/gitblit/GitBlit.java Прегледај датотеку

@@ -44,6 +44,7 @@ import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.Singleton;

/**
@@ -62,22 +63,23 @@ public class GitBlit extends GitblitManager {

@Inject
public GitBlit(
Provider<IPublicKeyManager> publicKeyManagerProvider,
IRuntimeManager runtimeManager,
IPluginManager pluginManager,
INotificationManager notificationManager,
IUserManager userManager,
IAuthenticationManager authenticationManager,
IPublicKeyManager publicKeyManager,
IRepositoryManager repositoryManager,
IProjectManager projectManager,
IFederationManager federationManager) {

super(runtimeManager,
super(
publicKeyManagerProvider,
runtimeManager,
pluginManager,
notificationManager,
userManager,
authenticationManager,
publicKeyManager,
repositoryManager,
projectManager,
federationManager);
@@ -122,24 +124,6 @@ public class GitBlit extends GitblitManager {
}
}

/**
* Delete the user and all associated public ssh keys.
*/
@Override
public boolean deleteUser(String username) {
UserModel user = userManager.getUserModel(username);
return deleteUserModel(user);
}

@Override
public boolean deleteUserModel(UserModel model) {
boolean success = userManager.deleteUserModel(model);
if (success) {
getPublicKeyManager().removeAllKeys(model.username);
}
return success;
}

/**
* Delete the repository and all associated tickets.
*/

+ 2
- 32
src/main/java/com/gitblit/guice/CoreModule.java Прегледај датотеку

@@ -18,7 +18,6 @@ package com.gitblit.guice;
import com.gitblit.FileSettings;
import com.gitblit.GitBlit;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.manager.AuthenticationManager;
import com.gitblit.manager.FederationManager;
import com.gitblit.manager.IAuthenticationManager;
@@ -38,14 +37,9 @@ import com.gitblit.manager.RepositoryManager;
import com.gitblit.manager.RuntimeManager;
import com.gitblit.manager.ServicesManager;
import com.gitblit.manager.UserManager;
import com.gitblit.transport.ssh.FileKeyManager;
import com.gitblit.transport.ssh.IPublicKeyManager;
import com.gitblit.transport.ssh.MemoryKeyManager;
import com.gitblit.transport.ssh.NullKeyManager;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.WorkQueue;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;

/**
* CoreModule references all the core business objects.
@@ -61,8 +55,9 @@ public class CoreModule extends AbstractModule {
bind(IStoredSettings.class).toInstance(new FileSettings());

// bind complex providers
bind(IPublicKeyManager.class).toProvider(IPublicKeyManagerProvider.class);
bind(WorkQueue.class).toProvider(WorkQueueProvider.class);
// core managers
bind(IRuntimeManager.class).to(RuntimeManager.class);
bind(IPluginManager.class).to(PluginManager.class);
@@ -79,29 +74,4 @@ public class CoreModule extends AbstractModule {
// manager for long-running daemons and services
bind(IServicesManager.class).to(ServicesManager.class);
}

@Provides
@Singleton
IPublicKeyManager providePublicKeyManager(IStoredSettings settings, IRuntimeManager runtimeManager) {

String clazz = settings.getString(Keys.git.sshKeysManager, FileKeyManager.class.getName());
if (StringUtils.isEmpty(clazz)) {
clazz = FileKeyManager.class.getName();
}
if (FileKeyManager.class.getName().equals(clazz)) {
return new FileKeyManager(runtimeManager);
} else if (NullKeyManager.class.getName().equals(clazz)) {
return new NullKeyManager();
} else if (MemoryKeyManager.class.getName().equals(clazz)) {
return new MemoryKeyManager();
} else {
try {
Class<?> mgrClass = Class.forName(clazz);
return (IPublicKeyManager) mgrClass.newInstance();
} catch (Exception e) {

}
return null;
}
}
}

+ 72
- 0
src/main/java/com/gitblit/guice/IPublicKeyManagerProvider.java Прегледај датотеку

@@ -0,0 +1,72 @@
/*
* Copyright 2014 gitblit.com.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.gitblit.guice;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.transport.ssh.FileKeyManager;
import com.gitblit.transport.ssh.IPublicKeyManager;
import com.gitblit.transport.ssh.NullKeyManager;
import com.gitblit.utils.StringUtils;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

/**
* Provides a lazily-instantiated IPublicKeyManager configured from IStoredSettings.
*
* @author James Moger
*
*/
@Singleton
public class IPublicKeyManagerProvider implements Provider<IPublicKeyManager> {

private final Logger logger = LoggerFactory.getLogger(getClass());

private final IRuntimeManager runtimeManager;

private volatile IPublicKeyManager manager;

@Inject
public IPublicKeyManagerProvider(IRuntimeManager runtimeManager) {
this.runtimeManager = runtimeManager;
}

@Override
public synchronized IPublicKeyManager get() {
if (manager != null) {
return manager;
}

IStoredSettings settings = runtimeManager.getSettings();
String clazz = settings.getString(Keys.git.sshKeysManager, FileKeyManager.class.getName());
if (StringUtils.isEmpty(clazz)) {
clazz = FileKeyManager.class.getName();
}
try {
Class<? extends IPublicKeyManager> mgrClass = (Class<? extends IPublicKeyManager>) Class.forName(clazz);
manager = runtimeManager.getInjector().getInstance(mgrClass);
} catch (Exception e) {
logger.error("failed to create public key manager", e);
manager = new NullKeyManager();
}
return manager;
}
}

+ 22
- 11
src/main/java/com/gitblit/manager/GitblitManager.java Прегледај датотеку

@@ -86,6 +86,7 @@ import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
import com.google.inject.Provider;

/**
* GitblitManager is an aggregate interface delegate. It implements all the manager
@@ -106,6 +107,8 @@ public class GitblitManager implements IGitblit {

protected final ObjectCache<Collection<GitClientApplication>> clientApplications = new ObjectCache<Collection<GitClientApplication>>();

protected final Provider<IPublicKeyManager> publicKeyManagerProvider;

protected final IStoredSettings settings;

protected final IRuntimeManager runtimeManager;
@@ -118,8 +121,6 @@ public class GitblitManager implements IGitblit {

protected final IAuthenticationManager authenticationManager;

protected final IPublicKeyManager publicKeyManager;

protected final IRepositoryManager repositoryManager;

protected final IProjectManager projectManager;
@@ -128,23 +129,24 @@ public class GitblitManager implements IGitblit {

@Inject
public GitblitManager(
Provider<IPublicKeyManager> publicKeyManagerProvider,
IRuntimeManager runtimeManager,
IPluginManager pluginManager,
INotificationManager notificationManager,
IUserManager userManager,
IAuthenticationManager authenticationManager,
IPublicKeyManager publicKeyManager,
IRepositoryManager repositoryManager,
IProjectManager projectManager,
IFederationManager federationManager) {

this.publicKeyManagerProvider = publicKeyManagerProvider;

this.settings = runtimeManager.getSettings();
this.runtimeManager = runtimeManager;
this.pluginManager = pluginManager;
this.notificationManager = notificationManager;
this.userManager = userManager;
this.authenticationManager = authenticationManager;
this.publicKeyManager = publicKeyManager;
this.repositoryManager = repositoryManager;
this.projectManager = projectManager;
this.federationManager = federationManager;
@@ -487,7 +489,7 @@ public class GitblitManager implements IGitblit {

@Override
public IPublicKeyManager getPublicKeyManager() {
return publicKeyManager;
return publicKeyManagerProvider.get();
}

/*
@@ -706,11 +708,6 @@ public class GitblitManager implements IGitblit {
return userManager.getAllUsers();
}

@Override
public boolean deleteUser(String username) {
return userManager.deleteUser(username);
}

@Override
public UserModel getUserModel(String username) {
return userManager.getUserModel(username);
@@ -751,9 +748,23 @@ public class GitblitManager implements IGitblit {
return userManager.updateUserModel(username, model);
}

@Override
public boolean deleteUser(String username) {
// delegate to deleteUserModel() to delete public ssh keys
UserModel user = userManager.getUserModel(username);
return deleteUserModel(user);
}

/**
* Delete the user and all associated public ssh keys.
*/
@Override
public boolean deleteUserModel(UserModel model) {
return userManager.deleteUserModel(model);
boolean success = userManager.deleteUserModel(model);
if (success) {
getPublicKeyManager().removeAllKeys(model.username);
}
return success;
}

@Override

+ 2
- 0
src/main/java/com/gitblit/transport/ssh/FileKeyManager.java Прегледај датотеку

@@ -29,6 +29,7 @@ import com.gitblit.manager.IRuntimeManager;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.io.Files;
import com.google.inject.Inject;

/**
* Manages public keys on the filesystem.
@@ -42,6 +43,7 @@ public class FileKeyManager extends IPublicKeyManager {

protected final Map<File, Long> lastModifieds;

@Inject
public FileKeyManager(IRuntimeManager runtimeManager) {
this.runtimeManager = runtimeManager;
this.lastModifieds = new ConcurrentHashMap<File, Long>();

+ 3
- 0
src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java Прегледај датотеку

@@ -20,6 +20,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.inject.Inject;

/**
* Memory public key manager.
*
@@ -30,6 +32,7 @@ public class MemoryKeyManager extends IPublicKeyManager {

final Map<String, List<SshKey>> keys;

@Inject
public MemoryKeyManager() {
keys = new HashMap<String, List<SshKey>>();
}

+ 3
- 0
src/main/java/com/gitblit/transport/ssh/NullKeyManager.java Прегледај датотеку

@@ -17,6 +17,8 @@ package com.gitblit.transport.ssh;

import java.util.List;

import com.google.inject.Inject;

/**
* Rejects all public key management requests.
*
@@ -25,6 +27,7 @@ import java.util.List;
*/
public class NullKeyManager extends IPublicKeyManager {

@Inject
public NullKeyManager() {
}


+ 6
- 5
src/main/java/com/gitblit/wicket/GitBlitWebApp.java Прегледај датотеку

@@ -91,6 +91,7 @@ import com.gitblit.wicket.pages.TreePage;
import com.gitblit.wicket.pages.UserPage;
import com.gitblit.wicket.pages.UsersPage;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

@Singleton
@@ -102,6 +103,8 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp {

private final Map<String, CacheControl> cacheablePages = new HashMap<String, CacheControl>();

private final Provider<IPublicKeyManager> publicKeyManagerProvider;

private final IStoredSettings settings;

private final IRuntimeManager runtimeManager;
@@ -114,8 +117,6 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp {

private final IAuthenticationManager authenticationManager;

private final IPublicKeyManager publicKeyManager;

private final IRepositoryManager repositoryManager;

private final IProjectManager projectManager;
@@ -128,12 +129,12 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp {

@Inject
public GitBlitWebApp(
Provider<IPublicKeyManager> publicKeyManagerProvider,
IRuntimeManager runtimeManager,
IPluginManager pluginManager,
INotificationManager notificationManager,
IUserManager userManager,
IAuthenticationManager authenticationManager,
IPublicKeyManager publicKeyManager,
IRepositoryManager repositoryManager,
IProjectManager projectManager,
IFederationManager federationManager,
@@ -141,13 +142,13 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp {
IServicesManager services) {

super();
this.publicKeyManagerProvider = publicKeyManagerProvider;
this.settings = runtimeManager.getSettings();
this.runtimeManager = runtimeManager;
this.pluginManager = pluginManager;
this.notificationManager = notificationManager;
this.userManager = userManager;
this.authenticationManager = authenticationManager;
this.publicKeyManager = publicKeyManager;
this.repositoryManager = repositoryManager;
this.projectManager = projectManager;
this.federationManager = federationManager;
@@ -389,7 +390,7 @@ public class GitBlitWebApp extends WebApplication implements GitblitWicketApp {
*/
@Override
public IPublicKeyManager keys() {
return publicKeyManager;
return publicKeyManagerProvider.get();
}

/* (non-Javadoc)

Loading…
Откажи
Сачувај