23 |
--------------------------------------------------------------------------------
/src/main/java/org/whispersystems/claserver/GithubUserResponse.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Open WhisperSystems
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 | package org.whispersystems.claserver;
18 |
19 | import org.codehaus.jackson.annotate.JsonIgnoreProperties;
20 |
21 | @JsonIgnoreProperties(ignoreUnknown = true)
22 | public class GithubUserResponse {
23 | public String login;
24 | public Long id;
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/org/whispersystems/claserver/GithubCreateStatus.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Open WhisperSystems
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 | package org.whispersystems.claserver;
18 |
19 | public class GithubCreateStatus {
20 | public String state;
21 | public String description;
22 | public String context;
23 | public String target_url;
24 |
25 | public GithubCreateStatus(String state, String description, String context, String target_url) {
26 | this.state = state;
27 | this.description = description;
28 | this.context = context;
29 | this.target_url = target_url;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/org/whispersystems/claserver/GithubPullEvent.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Open WhisperSystems
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 | package org.whispersystems.claserver;
18 |
19 | import org.codehaus.jackson.annotate.JsonIgnoreProperties;
20 |
21 | @JsonIgnoreProperties(ignoreUnknown=true)
22 | public class GithubPullEvent {
23 | @JsonIgnoreProperties(ignoreUnknown=true)
24 | public static class PullRequest {
25 | public String url;
26 | public String html_url;
27 | public User user;
28 | public String statuses_url;
29 | }
30 |
31 | @JsonIgnoreProperties(ignoreUnknown=true)
32 | public static class User {
33 | public String login;
34 | }
35 |
36 | public PullRequest pull_request;
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/org/whispersystems/claserver/RenderForm.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Open WhisperSystems
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 | package org.whispersystems.claserver;
18 |
19 | import com.github.mustachejava.DefaultMustacheFactory;
20 | import com.github.mustachejava.Mustache;
21 | import com.github.mustachejava.MustacheFactory;
22 |
23 | import java.io.File;
24 | import java.io.IOException;
25 | import java.io.StringWriter;
26 |
27 | public class RenderForm {
28 | public static void main(String[] args) throws IOException {
29 | MustacheFactory mf = new DefaultMustacheFactory(new File("src/main/webapp/WEB-INF/templates"));
30 | Mustache mustache = mf.compile("form_inner.mustache");
31 | StringWriter writer = new StringWriter();
32 | AuthFormController controller = AuthFormController.createEmptyFormController(mf);
33 | controller.baseUrl = "https://open-whisper-cla.appspot.com/cla-server";
34 | mustache.execute(writer, controller);
35 | writer.flush();
36 | System.out.println(writer.toString());
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Contributor License Agreement Server
2 | ==================
3 |
4 | This server is designed for contributors to electronically sign the agreement.
5 | When pull requests come in, the server sets a status depending on whether or
6 | not the Github user has signed.
7 |
8 | Setting up the Github webhook
9 | -------------------
10 | - The payload url should be https://xxx.appspot.com/cla-server/validate
11 | - The secret should match the key in the database described below
12 | - Select only the pull request event
13 |
14 | Building and deploying
15 | --------------------
16 | - To run locally: `mvn app engine:devserver`. However, you will need to set up an oauth client and redirect, etc to work.
17 | - To deploy: `mvn appengine:update`
18 |
19 | Keys
20 | -------------------
21 | In development, you can populate the keys in `development.json`.
22 |
23 | In production, you need to set the keys in the database as following:
24 |
25 | There are a few keys that need to be set in the database. They can be set using the [google developer console] (https://console.developers.google.com/). The entity name is "Secrets" and each of the following has a single property named "key". In addition, the CORS headers are set using the whispersystems-url here.
26 | - *github-webhook-secret* - the secret to validate requests coming in from the webhook
27 | - *github-user-token* - the token for the github user that updates the status of the pull request
28 | - *github-oauth-client-id* - the github oauth client id
29 | - *github-oauth-client-secret* - the github oauth client secret
30 |
31 | Rendering the form
32 | --------------------
33 | There is a class called RenderForm.java that can be used to generate the static html for the main site.
34 |
35 | License
36 | ---------------------
37 |
38 | Copyright 2013 Open Whisper Systems
39 |
40 | Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
41 |
--------------------------------------------------------------------------------
/src/main/java/org/whispersystems/claserver/AddResponse.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Open WhisperSystems
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 | package org.whispersystems.claserver;
18 |
19 | import com.google.appengine.repackaged.org.codehaus.jackson.annotate.JsonIgnoreProperties;
20 | import com.google.common.collect.ImmutableList;
21 |
22 | import java.util.Collections;
23 | import java.util.List;
24 |
25 | @JsonIgnoreProperties(ignoreUnknown=true)
26 | public class AddResponse {
27 | public static enum Status {
28 | ADDED,
29 | ERROR
30 | }
31 |
32 | private Status status;
33 | private List errorFields;
34 | private String authorizeUrl;
35 |
36 | public Status getStatus() {
37 | return status;
38 | }
39 |
40 | public void setStatus(Status status) {
41 | this.status = status;
42 | }
43 |
44 | public List getErrorFields() {
45 | return errorFields;
46 | }
47 |
48 | public void setErrorFields(List errorFields) {
49 | this.errorFields = errorFields;
50 | }
51 |
52 | public String getAuthorizeUrl() {
53 | return authorizeUrl;
54 | }
55 |
56 | public void setAuthorizeUrl(String authorizeUrl) {
57 | this.authorizeUrl = authorizeUrl;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/form_inner.mustache:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Please correct the highlighted errors.
5 |
6 |
7 |
19 |
20 |
--------------------------------------------------------------------------------
/src/main/java/org/whispersystems/claserver/UrlFetcher.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Open WhisperSystems
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 | package org.whispersystems.claserver;
18 |
19 | import com.google.common.io.CharStreams;
20 | import org.codehaus.jackson.map.ObjectMapper;
21 |
22 | import java.io.IOException;
23 | import java.io.InputStreamReader;
24 | import java.io.OutputStream;
25 | import java.net.HttpURLConnection;
26 | import java.net.URL;
27 | import java.util.Map;
28 | import java.util.logging.Logger;
29 |
30 | public class UrlFetcher {
31 | private static Logger logger = Logger.getLogger(UrlFetcher.class.getName());
32 | private static ObjectMapper mapper = new ObjectMapper();
33 |
34 | public static String post(String urlString, Object data, Map headers) throws IOException {
35 | URL url = new URL(urlString);
36 | HttpURLConnection connection = (HttpURLConnection) url.openConnection();
37 | connection.setDoOutput(true);
38 | connection.addRequestProperty("Content-Type", "application/json");
39 | connection.setRequestMethod("POST");
40 | for (Map.Entry header : headers.entrySet()) {
41 | connection.setRequestProperty(header.getKey(), header.getValue());
42 | }
43 | OutputStream outputStream = connection.getOutputStream();
44 | mapper.writeValue(outputStream, data);
45 | outputStream.close();
46 | logger.info(String.format("Status update response: %s", connection.getResponseCode()));
47 | return CharStreams.readLines(new InputStreamReader(connection.getInputStream())).get(0);
48 | }
49 |
50 | public T get(String urlString, Map headers, Class clazz) throws IOException {
51 | URL url = new URL(urlString);
52 | HttpURLConnection connection = (HttpURLConnection) url.openConnection();
53 | connection.setDoOutput(true);
54 | connection.addRequestProperty("Content-Type", "application/json");
55 | for (Map.Entry header : headers.entrySet()) {
56 | connection.setRequestProperty(header.getKey(), header.getValue());
57 | }
58 | logger.info(String.format("Status update response: %s", connection.getResponseCode()));
59 | return mapper.readValue(mapper.getJsonFactory().createJsonParser(connection.getInputStream()), clazz);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/org/whispersystems/claserver/Config.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Open WhisperSystems
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 | package org.whispersystems.claserver;
18 |
19 | import com.google.appengine.api.datastore.DatastoreService;
20 | import com.google.appengine.api.datastore.DatastoreServiceFactory;
21 | import com.google.appengine.api.datastore.EntityNotFoundException;
22 | import com.google.appengine.api.datastore.KeyFactory;
23 | import com.google.appengine.api.utils.SystemProperty;
24 | import org.codehaus.jackson.map.ObjectMapper;
25 |
26 | import java.io.IOException;
27 | import java.io.InputStream;
28 | import java.util.Map;
29 |
30 | public class Config {
31 | private DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
32 | public String githubSecret = "";
33 | public String githubUserToken = "";
34 | public String githubOauthClientId = "";
35 | public String githubOauthClientSecret = "";
36 | public String baseUrl;
37 | public String whisperSystemsUrl;
38 |
39 | private Config() throws IOException {
40 | try {
41 | InputStream stream = getClass().getResourceAsStream("/development.json");
42 | ObjectMapper mapper = new ObjectMapper();
43 | Map secrets = mapper.readValue(mapper.getJsonFactory().createJsonParser(stream), Map.class);
44 | githubSecret = getProp(secrets, "github-webhook-secret");
45 | githubUserToken = getProp(secrets, "github-user-token");
46 | githubOauthClientId = getProp(secrets, "github-oauth-client-id");
47 | githubOauthClientSecret = getProp(secrets, "github-oauth-client-secret");
48 | baseUrl = SystemProperty.environment.value() == SystemProperty.Environment.Value.Production ?
49 | "https://open-whisper-cla.appspot.com/cla-server" : getProp(secrets, "base-url");
50 | whisperSystemsUrl = getProp(secrets, "whispersystems-url");
51 | } catch (Exception ignored) {
52 | }
53 | }
54 |
55 | private String getProp(Map secrets, String name) throws EntityNotFoundException, IOException {
56 | if (SystemProperty.environment.value() == SystemProperty.Environment.Value.Production) {
57 | return (String) datastore.get(KeyFactory.createKey("Secrets", name)).getProperties().get("key");
58 | } else {
59 | return (String) secrets.get(name);
60 | }
61 | }
62 |
63 | private static Config instance = null;
64 |
65 | public static Config getInstance() {
66 | if (instance == null) {
67 | try {
68 | instance = new Config();
69 | } catch (IOException e) {
70 | e.printStackTrace();
71 | }
72 | }
73 | return instance;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/java/org/whispersystems/claserver/OauthCallbackServlet.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Open WhisperSystems
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 | package org.whispersystems.claserver;
18 |
19 | import com.google.appengine.api.datastore.DatastoreService;
20 | import com.google.appengine.api.datastore.DatastoreServiceFactory;
21 | import com.google.appengine.api.datastore.Entity;
22 | import com.google.appengine.api.datastore.EntityNotFoundException;
23 | import com.google.appengine.api.datastore.KeyFactory;
24 | import com.google.appengine.api.memcache.MemcacheService;
25 | import com.google.appengine.api.memcache.MemcacheServiceFactory;
26 |
27 | import javax.servlet.ServletException;
28 | import javax.servlet.http.HttpServlet;
29 | import javax.servlet.http.HttpServletRequest;
30 | import javax.servlet.http.HttpServletResponse;
31 | import java.io.IOException;
32 | import java.util.Collections;
33 | import java.util.HashMap;
34 | import java.util.Map;
35 |
36 | /**
37 | * This servlet handles the response after the user has properly authenticated with github. If successful, it
38 | * updates the user entity with the access token and github user.
39 | *
40 | * @author Tina Huang
41 | */
42 | public class OauthCallbackServlet extends HttpServlet {
43 | private DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
44 | private Config config = Config.getInstance();
45 |
46 | protected void doGet(HttpServletRequest request, HttpServletResponse resp) throws ServletException, IOException {
47 | String id = request.getParameter("id");
48 | MemcacheService memcacheService = MemcacheServiceFactory.getMemcacheService();
49 | String state = (String) memcacheService.get(id);
50 |
51 | // Make sure the state matches to prevent XSRF
52 | if (state.equals(request.getParameter("state"))) {
53 | Map params = new HashMap<>();
54 | params.put("client_id", config.githubOauthClientId);
55 | params.put("client_secret", config.githubOauthClientSecret);
56 | params.put("code", request.getParameter("code"));
57 | params.put("redirect_uri", config.baseUrl + "/oauth?redirect_url=" + request.getParameter("redirect_url"));
58 |
59 | String returnParams = UrlFetcher.post("https://github.com/login/oauth/access_token", params,
60 | Collections.emptyMap());
61 | String[] pairs = returnParams.split("&");
62 | for (String pair1 : pairs) {
63 | String[] pair = pair1.split("=");
64 | if (pair[0].equals("access_token")) {
65 | try {
66 | Entity user = datastore.get(KeyFactory.createKey("User", Long.parseLong(id)));
67 | user.setProperty("accessToken", pair[1]);
68 | GithubUserResponse response = new UrlFetcher().get("https://api.github.com/user?access_token=" + pair[1],
69 | Collections.singletonMap("User-Agent", "openwhispersystems"), GithubUserResponse.class);
70 | user.setProperty("githubUser", response.login);
71 | datastore.put(user);
72 | String urlParam = request.getParameter("redirect_url");
73 | if (urlParam != null && !urlParam.equals("null")) {
74 | resp.sendRedirect(config.baseUrl + "/validate?redirect_url=" + urlParam);
75 | } else {
76 | resp.sendRedirect(config.whisperSystemsUrl + "/cla/success/");
77 | }
78 | } catch (EntityNotFoundException e) {
79 | e.printStackTrace();
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/java/org/whispersystems/claserver/AuthFormController.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Open WhisperSystems
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 | package org.whispersystems.claserver;
18 |
19 | import com.github.mustachejava.Mustache;
20 | import com.github.mustachejava.MustacheFactory;
21 | import com.google.common.collect.ImmutableList;
22 |
23 | import java.io.IOException;
24 | import java.io.StringWriter;
25 | import java.util.List;
26 |
27 | class AuthFormController {
28 | static class FormInput {
29 | public Boolean twoCol;
30 | public String name;
31 | public String placeholder;
32 | public String html;
33 |
34 | FormInput() {
35 | }
36 |
37 | FormInput(Boolean twoCol, String name, String placeholder) {
38 | this.twoCol = twoCol;
39 | this.name = name;
40 | this.placeholder = placeholder;
41 | }
42 | }
43 |
44 | static class FormItem {
45 | public String labelFor;
46 | public String label;
47 | public boolean req;
48 | public List inputs;
49 |
50 | FormItem(String labelFor, String label, boolean req, List inputs) {
51 | this.labelFor = labelFor;
52 | this.label = label;
53 | this.req = req;
54 | this.inputs = inputs;
55 | }
56 | }
57 |
58 | public List items;
59 | public String baseUrl;
60 |
61 | AuthFormController(List items) {
62 | this.items = items;
63 | Config config = Config.getInstance();
64 | baseUrl = config.baseUrl;
65 | }
66 |
67 | public List items() {
68 | return items;
69 | }
70 |
71 | public static AuthFormController createEmptyFormController(MustacheFactory mf) throws IOException {
72 | FormInput countryInput = new FormInput();
73 | Mustache mustache = mf.compile("countries.mustache");
74 | StringWriter writer = new StringWriter();
75 | mustache.execute(writer, new Object()).flush();
76 | countryInput.html = writer.toString();
77 | countryInput.name = "country";
78 | countryInput.twoCol = true;
79 |
80 | ImmutableList items = ImmutableList.of(
81 | new FormItem("firstName", "Full Name", true, ImmutableList.of(
82 | new FormInput(true, "firstName", "First Name"),
83 | new FormInput(true, "lastName", "Last Name"))),
84 | new FormItem("email", "Email", true, ImmutableList.of(
85 | new FormInput(false, "email", ""))),
86 | new FormItem("address1", "Mailing Address", true, ImmutableList.of(
87 | new FormInput(false, "address1", "Street address"))),
88 | new FormItem(null, null, true, ImmutableList.of(
89 | new FormInput(false, "address2", "Address line 2"))),
90 | new FormItem(null, null, false, ImmutableList.of(
91 | new FormInput(true, "city", "City"),
92 | new FormInput(true, "state", "State / Province / Region"))),
93 | new FormItem(null, null, true, ImmutableList.of(
94 | new FormInput(true, "zip", "Postal / Zip Code"),
95 | countryInput)),
96 | new FormItem("phone", "Phone Number", true, ImmutableList.of(
97 | new FormInput(false, "phone", ""))),
98 | new FormItem("signature", "Electronic Signature: Type \"I AGREE\" to accept the terms above", true,
99 | ImmutableList.of(new FormInput(false, "signature", "I AGREE")))
100 | );
101 | return new AuthFormController(items);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | 4.0.0
6 | war
7 | 1.0-SNAPSHOT
8 |
9 | org.whispersystems
10 | cla-server
11 |
12 |
13 | 1
14 | UTF-8
15 |
16 |
17 |
18 | 3.1.0
19 |
20 |
21 |
22 |
23 |
24 | com.google.appengine
25 | appengine-api-1.0-sdk
26 | 1.9.17
27 |
28 |
29 | javax.servlet
30 | servlet-api
31 | 2.5
32 | provided
33 |
34 |
35 | javax.inject
36 | javax.inject
37 | 1
38 |
39 |
40 | com.github.spullara.mustache.java
41 | compiler
42 | 0.8.6
43 |
44 |
45 | org.codehaus.jackson
46 | jackson-mapper-asl
47 | 1.9.13
48 |
49 |
50 | commons-codec
51 | commons-codec
52 | 1.10
53 |
54 |
55 |
56 |
57 | junit
58 | junit
59 | 4.11
60 | test
61 |
62 |
63 | org.mockito
64 | mockito-all
65 | 1.9.5
66 | test
67 |
68 |
69 | com.google.appengine
70 | appengine-testing
71 | 1.9.17
72 | test
73 |
74 |
75 | com.google.appengine
76 | appengine-api-stubs
77 | 1.9.17
78 | test
79 |
80 |
81 |
82 |
83 |
84 | ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
85 |
86 |
87 | org.codehaus.mojo
88 | versions-maven-plugin
89 | 2.1
90 |
91 |
92 | compile
93 |
94 | display-dependency-updates
95 | display-plugin-updates
96 |
97 |
98 |
99 |
100 |
101 | org.apache.maven.plugins
102 | 3.1
103 | maven-compiler-plugin
104 |
105 | 1.7
106 | 1.7
107 |
108 |
109 |
110 | com.google.appengine
111 | appengine-maven-plugin
112 | 1.9.17
113 |
114 |
115 | org.apache.maven.plugins
116 | maven-war-plugin
117 | 2.4
118 |
119 | ${basedir}/src/main/webapp/WEB-INF/app.yaml
120 |
121 |
122 | ${basedir}/src/main/webapp/WEB-INF
123 | true
124 | WEB-INF
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/src/main/java/org/whispersystems/claserver/SignupServlet.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Open WhisperSystems
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 | package org.whispersystems.claserver;
18 |
19 | import com.github.mustachejava.DefaultMustacheFactory;
20 | import com.github.mustachejava.Mustache;
21 | import com.github.mustachejava.MustacheFactory;
22 | import com.google.appengine.api.datastore.DatastoreService;
23 | import com.google.appengine.api.datastore.DatastoreServiceFactory;
24 | import com.google.appengine.api.datastore.Entity;
25 | import com.google.appengine.api.datastore.Key;
26 | import com.google.appengine.api.memcache.MemcacheService;
27 | import com.google.appengine.api.memcache.MemcacheServiceFactory;
28 | import com.google.appengine.repackaged.org.joda.time.DateTime;
29 | import com.google.appengine.repackaged.org.joda.time.format.DateTimeFormatter;
30 | import com.google.appengine.repackaged.org.joda.time.format.ISODateTimeFormat;
31 | import org.codehaus.jackson.map.ObjectMapper;
32 |
33 | import javax.servlet.ServletException;
34 | import javax.servlet.http.HttpServlet;
35 | import javax.servlet.http.HttpServletRequest;
36 | import javax.servlet.http.HttpServletResponse;
37 | import java.io.File;
38 | import java.io.IOException;
39 | import java.io.StringWriter;
40 | import java.net.URLEncoder;
41 | import java.util.ArrayList;
42 | import java.util.Arrays;
43 | import java.util.Collections;
44 | import java.util.List;
45 | import java.util.Map;
46 | import java.util.UUID;
47 |
48 | /**
49 | * This servlet handles serving the CLA form (though in the WhisperSystems deploy, that form is inlined in the
50 | * static site and this servlet handles the ajax post). When the form is submitted, the server stores the user
51 | * information, but it is invalid until the user has properly authorized against github and the username has been
52 | * stored.
53 | *
54 | * @author Tina Huang
55 | */
56 | public class SignupServlet extends HttpServlet {
57 | MustacheFactory mf = new DefaultMustacheFactory(new File("WEB-INF/templates"));
58 | Mustache mustache = mf.compile("form.mustache");
59 | private DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
60 | private static final ObjectMapper mapper = new ObjectMapper();
61 | private static List required = Arrays.asList("firstName", "lastName", "email", "address1", "city", "state",
62 | "zip", "country", "phone");
63 | private DateTimeFormatter fmt = ISODateTimeFormat.dateTime();
64 | private Config config = Config.getInstance();
65 |
66 | protected void doPost(HttpServletRequest request, HttpServletResponse response)
67 | throws ServletException, IOException {
68 | Entity user = new Entity("User");
69 | AddResponse addResponse = new AddResponse();
70 | Map parameterMap = mapper.readValue(mapper.getJsonFactory().createJsonParser(request.getInputStream()), Map.class);
71 | List errorFields = validateAndPopulateUser(parameterMap);
72 | if (errorFields.size() == 0) {
73 | DateTime dt = new DateTime();
74 | parameterMap.put("timestamp", fmt.print(dt));
75 | parameterMap.put("ip", request.getRemoteAddr());
76 | StringWriter jsonProperties = new StringWriter();
77 | mapper.writeValue(mapper.getJsonFactory().createJsonGenerator(jsonProperties), parameterMap);
78 | user.setProperty("details", jsonProperties.toString());
79 | Key key = datastore.put(user);
80 | addResponse.setStatus(AddResponse.Status.ADDED);
81 | String id = String.valueOf(key.getId());
82 | String state = UUID.randomUUID().toString();
83 | MemcacheService memcacheService = MemcacheServiceFactory.getMemcacheService();
84 | memcacheService.put(id, state);
85 | StringBuilder url = new StringBuilder("https://github.com/login/oauth/authorize?client_id=");
86 | String redirectUrl = config.baseUrl + "/oauth?id=" + id + "&redirect_url=" + request.getParameter("redirect_url");
87 | url.append(config.githubOauthClientId);
88 | url.append("&redirect_uri=");
89 | url.append(URLEncoder.encode(redirectUrl, "UTF-8"));
90 | url.append("&state=");
91 | url.append(state);
92 | addResponse.setAuthorizeUrl(url.toString());
93 | } else {
94 | addResponse.setStatus(AddResponse.Status.ERROR);
95 | }
96 | addResponse.setErrorFields(errorFields);
97 | response.addHeader("Access-Control-Allow-Origin", config.whisperSystemsUrl);
98 | response.setContentType("application/json");
99 | mapper.writeValue(response.getWriter(), addResponse);
100 | response.getWriter().flush();
101 | }
102 |
103 | private List validateAndPopulateUser(Map parameterMap) throws IOException {
104 | List errorFields = new ArrayList<>();
105 | for (String s : required) {
106 | String param = (String) parameterMap.get(s);
107 | if (param == null || param.isEmpty()) {
108 | errorFields.add(s);
109 | }
110 | }
111 |
112 | String signature = (String) parameterMap.get("signature");
113 | if (signature == null || !signature.equals("I AGREE")) {
114 | errorFields.add("signature");
115 | }
116 | return errorFields;
117 | }
118 |
119 | /**
120 | * Serves up a standalone version of the CLA form.
121 | */
122 | protected void doGet(HttpServletRequest request, HttpServletResponse resp) throws ServletException, IOException {
123 | resp.setContentType("text/html");
124 | mustache.execute(resp.getWriter(), AuthFormController.createEmptyFormController(mf)).flush();
125 | }
126 |
127 | @Override
128 | protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
129 | resp.setHeader("Access-Control-Allow-Origin", config.whisperSystemsUrl);
130 | resp.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
131 | resp.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/main/java/org/whispersystems/claserver/PullRequestValidationServlet.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2015 Open WhisperSystems
3 | *
4 | * This program is free software: you can redistribute it and/or modify
5 | * it under the terms of the GNU Affero General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | * GNU Affero General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Affero General Public License
15 | * along with this program. If not, see .
16 | */
17 | package org.whispersystems.claserver;
18 |
19 | import com.google.appengine.api.datastore.DatastoreService;
20 | import com.google.appengine.api.datastore.DatastoreServiceFactory;
21 | import com.google.appengine.api.datastore.FetchOptions;
22 | import com.google.appengine.api.datastore.PreparedQuery;
23 | import com.google.appengine.api.datastore.Query;
24 | import com.google.appengine.repackaged.org.apache.commons.codec.binary.Hex;
25 | import com.google.common.io.CharStreams;
26 | import org.codehaus.jackson.map.ObjectMapper;
27 |
28 | import javax.crypto.Mac;
29 | import javax.crypto.spec.SecretKeySpec;
30 | import javax.servlet.ServletException;
31 | import javax.servlet.http.HttpServlet;
32 | import javax.servlet.http.HttpServletRequest;
33 | import javax.servlet.http.HttpServletResponse;
34 | import java.io.IOException;
35 | import java.io.StringWriter;
36 | import java.io.UnsupportedEncodingException;
37 | import java.net.URLEncoder;
38 | import java.security.InvalidKeyException;
39 | import java.security.MessageDigest;
40 | import java.security.NoSuchAlgorithmException;
41 | import java.util.HashMap;
42 | import java.util.Map;
43 | import java.util.logging.Logger;
44 | import org.apache.commons.codec.binary.Base64;
45 |
46 | /**
47 | * This class validates if the owner of a pull request has signed the CLA and updates the github status accordingly.
48 | *
49 | * @author Tina Huang
50 | */
51 | public class PullRequestValidationServlet extends HttpServlet {
52 | private DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
53 | private Logger logger = Logger.getLogger(SignupServlet.class.getName());
54 | private ObjectMapper mapper = new ObjectMapper();
55 | private Config config = Config.getInstance();
56 |
57 | public GithubCreateStatus getStatus(boolean success, GithubPullEvent.PullRequest event) throws UnsupportedEncodingException {
58 | String url = config.baseUrl + "/validate?redirect_url=" + URLEncoder.encode(event.url, "UTF-8");
59 | if (success) {
60 | return new GithubCreateStatus(
61 | "success",
62 | "Contributor License Agreement signed",
63 | "cla-server/validation",
64 | config.whisperSystemsUrl + "/cla");
65 | } else {
66 | return new GithubCreateStatus(
67 | "failure",
68 | "Please sign the Contributor License Agreement",
69 | "cla-server/validation",
70 | url);
71 | }
72 | }
73 |
74 | /**
75 | * This is the endpoint for the github webhook
76 | */
77 | protected void doPost(HttpServletRequest request, HttpServletResponse resp) throws ServletException, IOException {
78 | String eventType = request.getHeader("X-GitHub-Event");
79 | if (eventType.equals("pull_request")) {
80 | String xHubSig = request.getHeader("X-Hub-Signature");
81 | StringWriter writer = new StringWriter();
82 | mapper.writeValue(writer, request.getParameterMap());
83 | String body = CharStreams.toString(request.getReader());
84 | GithubPullEvent event = mapper.readValue(mapper.getJsonFactory().createJsonParser(body), GithubPullEvent.class);
85 |
86 | try {
87 | Mac mac = Mac.getInstance("HmacSHA1");
88 | SecretKeySpec secret = new SecretKeySpec(config.githubSecret.getBytes("UTF-8"), "HmacSHA1");
89 | mac.init(secret);
90 | byte[] digest = mac.doFinal(body.getBytes());
91 | String hmac = String.format("sha1=%s", Hex.encodeHexString(digest));
92 |
93 | if (MessageDigest.isEqual(hmac.getBytes(), xHubSig.getBytes())) {
94 | updateStatus(config, event.pull_request);
95 | } else {
96 | logger.warning("Invalid request signature");
97 | }
98 | } catch (NoSuchAlgorithmException | InvalidKeyException e) {
99 | e.printStackTrace();
100 | }
101 | }
102 | }
103 |
104 | private boolean updateStatus(Config config, GithubPullEvent.PullRequest event) throws IOException {
105 | Query.FilterPredicate filter = new Query.FilterPredicate("githubUser",
106 | Query.FilterOperator.EQUAL, event.user.login);
107 | Query query = new Query("User").setFilter(filter);
108 | PreparedQuery pq = datastore.prepare(query);
109 | boolean success = pq.countEntities(FetchOptions.Builder.withLimit(1)) > 0;
110 | GithubCreateStatus status = getStatus(success, event);
111 | UrlFetcher.post(event.statuses_url, status, getAuthorization(config));
112 | return success;
113 | }
114 |
115 | private Map getAuthorization(Config keyStore) {
116 | Map headers = new HashMap<>();
117 | byte[] authBytes = String.format("%s:x-oauth-basic", keyStore.githubUserToken).getBytes();
118 | String basicAuth = "Basic " + Base64.encodeBase64String(authBytes);;
119 | headers.put("Authorization", basicAuth);
120 | return headers;
121 | }
122 |
123 | /**
124 | * This is the destination for the details link for a pull request that has failed the CLA check. When
125 | * clicked, it checks if the user has since signed the CLA, and if not, it redirects the user to the CLA.
126 | */
127 | protected void doGet(HttpServletRequest request, HttpServletResponse resp) throws ServletException, IOException {
128 | String url = request.getParameter("redirect_url");
129 | Map headers = getAuthorization(config);
130 | headers.put("User-Agent", "openwhispersystems");
131 | GithubPullEvent.PullRequest event = new UrlFetcher().get(url, headers,
132 | GithubPullEvent.PullRequest.class);
133 | boolean status = updateStatus(config, event);
134 | if (status) {
135 | resp.sendRedirect(event.html_url);
136 | } else {
137 | resp.sendRedirect(config.whisperSystemsUrl + "/cla?redirect_url=" + url);
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/countries.mustache:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------