├── src └── main │ ├── webapp │ └── WEB-INF │ │ ├── templates │ │ ├── form.mustache │ │ ├── form_row.mustache │ │ ├── form_inner.mustache │ │ └── countries.mustache │ │ ├── logging.properties │ │ └── app.yaml │ ├── resources │ └── development.json │ └── java │ └── org │ └── whispersystems │ └── claserver │ ├── GithubUserResponse.java │ ├── GithubCreateStatus.java │ ├── GithubPullEvent.java │ ├── RenderForm.java │ ├── AddResponse.java │ ├── UrlFetcher.java │ ├── Config.java │ ├── OauthCallbackServlet.java │ ├── AuthFormController.java │ ├── SignupServlet.java │ └── PullRequestValidationServlet.java ├── README.md └── pom.xml /src/main/webapp/WEB-INF/templates/form.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{>form_inner}} 7 | 8 | -------------------------------------------------------------------------------- /src/main/resources/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "github-webhook-secret": "", 3 | "github-user-token": "", 4 | "github-oauth-client-id": "", 5 | "github-oauth-client-secret": "", 6 | "base-url": "http://localhost:8082/cla-server", 7 | "whispersystems-url": "http://127.0.0.1:4000" 8 | } 9 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/logging.properties: -------------------------------------------------------------------------------- 1 | # A default java.util.logging configuration. 2 | # (All App Engine logging is through java.util.logging by default). 3 | # 4 | # To use this configuration, copy it into your application's WEB-INF 5 | # folder and add the following to your appengine-web.xml: 6 | # 7 | # 8 | # 9 | # 10 | # 11 | 12 | # Set the default logging level for all loggers to WARNING 13 | .level = ALL 14 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/app.yaml: -------------------------------------------------------------------------------- 1 | application: open-whisper-cla 2 | version: 1 3 | runtime: java 4 | threadsafe: true 5 | 6 | inbound_services: 7 | - warmup 8 | 9 | handlers: 10 | - url: /admin/* 11 | login: admin 12 | 13 | - url: /cla-server/form 14 | servlet: org.whispersystems.claserver.SignupServlet 15 | name: cla 16 | secure: always 17 | 18 | - url: /cla-server/validate 19 | servlet: org.whispersystems.claserver.PullRequestValidationServlet 20 | name: validate 21 | secure: always 22 | 23 | - url: /cla-server/oauth 24 | servlet: org.whispersystems.claserver.OauthCallbackServlet 25 | name: oauth 26 | secure: always 27 | 28 | - url: /remote_api 29 | servlet: com.google.apphosting.utils.remoteapi.RemoteApiServlet 30 | name: remote 31 | secure: always 32 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/templates/form_row.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{#label}} 4 | 7 | {{/label}} 8 | {{^label}} 9 |
 
10 | {{/label}} 11 | 12 | {{#inputs}} 13 | 14 | {{#html}} 15 | {{{html}}} 16 | {{/html}} 17 | {{^html}} 18 | 19 | {{/html}} 20 | 21 | {{/inputs}} 22 | 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 | 7 |
8 | 9 | {{#items}} 10 | {{>form_row}} 11 | {{/items}} 12 |
13 | 14 |
15 | 16 | 17 |
18 |
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 | --------------------------------------------------------------------------------