Created
April 22, 2014 16:10
-
-
Save bojanrajkovic/11185047 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<%@ WebHandler Language="c#" Class="Xamarin.Activation.Studio" %> | |
using System; | |
using System.Collections.Generic; | |
using System.Data; | |
using System.IO; | |
using System.Linq; | |
using System.Web; | |
using System.Net; | |
using System.Text; | |
using MySql.Data.MySqlClient; | |
using System.Configuration; | |
using System.Collections.Generic; | |
using System.Collections.Specialized; | |
using Newtonsoft.Json; | |
using Mono.Touch.Activation.Server; | |
using Mono.Touch.Activation.Common; | |
using CustomerPortal; | |
using Xamarin.SSO.Client; | |
using System.Security.Cryptography; | |
using Segmentio; | |
using Segmentio.Model; | |
namespace Xamarin.Activation | |
{ | |
public class Studio : IHttpHandler | |
{ | |
const string Passphrase = "22BA3F38-7552-47BC-9548-ECA8AFB9356D"; | |
const int IterationCount = 2000; | |
const int KeyLengthBits = 256; // Bits | |
const int SaltLength = 8; // Bytes | |
static readonly Client Analytics = new Client (ConfigurationManager.AppSettings["SegmentIoApiKey"] ?? "63thby1wjn4g5yfsvbjy"); | |
static string DecryptString (string cipherString) | |
{ | |
var input = Convert.FromBase64String (cipherString); | |
// First 16 bytes are the IV, then the next 8 are the salt, then the rest is the ciphertext. | |
byte[] salt = new byte[SaltLength], iv = new byte[16], ciphertext = new byte[input.Length - 24]; | |
Buffer.BlockCopy (input, 0, iv, 0, 16); | |
Buffer.BlockCopy (input, 16, salt, 0, 8); | |
Buffer.BlockCopy (input, 24, ciphertext, 0, input.Length - 24); | |
byte[] key = DeriveKey (salt); | |
byte[] plainBytes = null; | |
using (var aes = new AesCryptoServiceProvider ()) { | |
using (var ms = new MemoryStream ()) { | |
var transform = aes.CreateDecryptor (key, iv); | |
using (var cs = new CryptoStream (ms, transform, CryptoStreamMode.Write)) | |
cs.Write (ciphertext, 0, ciphertext.Length); | |
plainBytes = ms.ToArray (); | |
} | |
} | |
return Encoding.UTF8.GetString (plainBytes); | |
} | |
static byte[] DeriveKey (byte[] salt) | |
{ | |
var kdf = new Rfc2898DeriveBytes (Passphrase, salt, IterationCount); | |
return kdf.GetBytes (KeyLengthBits / 8); | |
} | |
public bool IsReusable { | |
get { return true; } | |
} | |
public void ProcessRequest (HttpContext context) | |
{ | |
var request = context.Request; | |
var response = context.Response; | |
Activate (request, response); | |
} | |
private static string GetRequestBody (HttpRequest Request) | |
{ | |
string body; | |
using (Stream receiveStream = Request.InputStream) { | |
using (StreamReader readStream = new StreamReader (receiveStream, Encoding.UTF8)) { | |
body = readStream.ReadToEnd(); | |
} | |
} | |
return body; | |
} | |
static readonly int[] ProductIdsThatTrumpTrials = new [] { 1, 12, 19, 20, 23, 26 }; | |
static void Activate (HttpRequest req, HttpResponse resp) | |
{ | |
resp.ContentType = "application/json"; | |
var actualData = GetRequestBody (req); | |
var token = req.QueryString["token"]; | |
var guid = req.QueryString["guid"]; | |
var product = req.QueryString["product"]; | |
var db = new DB (); | |
resp.StatusCode = 200; | |
if (string.IsNullOrWhiteSpace (guid) || string.IsNullOrWhiteSpace (token) || string.IsNullOrWhiteSpace (product) || string.IsNullOrWhiteSpace (actualData)) { | |
// TODO: Figure out what to send to analytics here | |
resp.Write (JsonConvert.SerializeObject (new { code = -1, message = "Missing data." })); | |
return; | |
} | |
Tuple<string, string> userCodes; | |
string file = DecryptString (actualData), name, email, company, phone, activation_code; | |
Xamarin.SSO.Client.User user = null; | |
// Try to get the user via direct DB access | |
try { | |
var localUser = db.GetUserInfo (token, guid); | |
if (localUser != null) { | |
user = new Xamarin.SSO.Client.User { | |
Guid = localUser.Guid, | |
Email = localUser.Email, | |
FirstName = localUser.FirstName, | |
LastName = localUser.LastName, | |
Company = localUser.Company, | |
PhoneNumber = localUser.PhoneNumber, | |
}; | |
} else { user = null; } | |
} catch { | |
user = null; | |
} | |
var ssoApiUrl = ConfigurationManager.AppSettings["SSOApiUrl"]; | |
var client = new XamarinSSOClient (ssoApiUrl, ConfigurationManager.AppSettings["SSOApiKey"]); | |
if (user == null) { | |
try { | |
Console.Error.WriteLine ("Looking up token {0}", token); | |
var tokenInfo = client.GetTokenInformation (token); | |
user = tokenInfo.User; | |
} catch (Exception e) { | |
resp.Write (JsonConvert.SerializeObject (new { code = -2, message = "Failed to look up user.", messageDetail = e.Message })); | |
return; | |
} | |
} | |
if (user == null) { | |
// TODO: Figure out what to send to analytics here | |
resp.Write (JsonConvert.SerializeObject (new { code = -2, message = "Could not find user with matching ID." })); | |
return; | |
} | |
if (user.Guid != guid) { | |
// TODO: Figure out what to send to analytics here | |
resp.Write (JsonConvert.SerializeObject (new { code = -2, message = "User doesn't match token." })); | |
return; | |
} | |
name = user.FirstName + " " + user.LastName; | |
email = user.Email; | |
company = user.Company; | |
phone = user.PhoneNumber; | |
// Now we need to look up activation codes for this user... | |
userCodes = db.GetActivationCodeForUser (product, guid); | |
if (string.IsNullOrWhiteSpace (userCodes.Item1) && string.IsNullOrWhiteSpace (userCodes.Item2)) { | |
// Try forcing auto-assignment from the store. | |
try { | |
var wc = new WebClient (); | |
var host = new Uri (ssoApiUrl).Host; | |
var storeDomain = host.Substring (host.IndexOf ('.')); | |
var storeName = storeDomain != ".xamarin.com" ? "taco" : "store"; | |
var res = wc.DownloadString (string.Format ("https://{0}{1}/api/getlicensesforuser?guid={2}", storeName, storeDomain, user.Guid)); | |
userCodes = db.GetActivationCodeForUser (product, guid); | |
// OK, auto-assignment should have happened, but it apparently didn't. | |
if (string.IsNullOrWhiteSpace (userCodes.Item1) && string.IsNullOrWhiteSpace (userCodes.Item2)) { | |
TrackError (user, -3, "Could not look up activation code."); | |
resp.Write (JsonConvert.SerializeObject (new { code = -3, message = "Could not look up activation code." })); | |
return; | |
} | |
} catch (Exception e) { | |
TrackError (user, -3, "Could not look up activation code."); | |
resp.Write (JsonConvert.SerializeObject (new { code = -3, message = "Could not look up activation code." })); | |
return; | |
} | |
} | |
var realInfo = userCodes.Item1 != null ? db.GetLicenseData (userCodes.Item1) : null; | |
var trialInfo = userCodes.Item2 != null ? db.GetLicenseData (userCodes.Item2) : null; | |
bool didReturnTrial; | |
if (realInfo == null && trialInfo == null) { | |
TrackError (user, -3, "Could not look up extended information for license."); | |
resp.Write (JsonConvert.SerializeObject (new { code = -3, message = "Could not look up extended information for license." })); | |
return; | |
} | |
if (trialInfo != null) { | |
// OK, they have a trial that's not disabled. Does it trump their license? | |
if (realInfo == null) { | |
// Real info is null, return the trial. | |
activation_code = userCodes.Item2; | |
didReturnTrial = true; | |
} else { | |
// Does the real license trump the trial? | |
int realProductId = realInfo.ProductId; | |
if (ProductIdsThatTrumpTrials.Contains (realProductId) || (trialInfo.ValidUntil > new DateTime (2001, 1, 1, 0, 0, 0) && trialInfo.ValidUntil < DateTime.UtcNow)) { | |
didReturnTrial = false; | |
activation_code = userCodes.Item1; | |
} else { | |
didReturnTrial = true; | |
activation_code = userCodes.Item2; | |
} | |
} | |
} else { | |
didReturnTrial = false; | |
activation_code = userCodes.Item1; | |
} | |
var data = new UserData () { | |
Name = name, | |
Email = email, | |
Company = company ?? String.Empty, | |
Phone = phone ?? String.Empty, | |
ActivationCode = activation_code, | |
ProductVersion = -1, | |
DataFile = file | |
}; | |
var ip = ServerUtil.GetClientIP (req.UserHostAddress, req.Headers["X-Forwarded-For"]); | |
var service = new ActivationService (); | |
var response = service.ApiActivate (product, data, ip); | |
var success = response.ResponseCode == ActivationResponseCode.Success; | |
int usedSlots = 0, availableSlots = 0; | |
db.GetActivationSlotCountsForUser (guid, ref usedSlots, ref availableSlots); | |
if (!success) { | |
DbUtils.Log ("error", "activation", ip, "Didn't activate anything"); | |
const string msg_already_in_use = "This registration code is already in use. " + | |
"If you need to transfer this license to another machine, " + | |
"please visit https://store.xamarin.com/account/products"; | |
if (response.Message.Equals (msg_already_in_use, StringComparison.OrdinalIgnoreCase)) { | |
TrackError (user, -4, "You have reached the maximum number of activations."); | |
resp.Write (JsonConvert.SerializeObject (new { code = -4, message = "You have reached the maximum number of activations.", usedMachines = usedSlots, allowedMachines = availableSlots })); | |
return; | |
} | |
TrackError (user, (int) response.ResponseCode, response.Message); | |
resp.Write (JsonConvert.SerializeObject (new { code = response.ResponseCode, message = response.Message, messageDetail = response.Description, license = (string) null })); | |
return; | |
} | |
var info = didReturnTrial ? trialInfo : realInfo; | |
int product_id = info.ProductId; | |
string valid_until = info.ValidUntil.ToString ("u"); | |
string level = GetProductLevelString (product_id); | |
var couldDowngradeTo = realInfo != null && didReturnTrial ? GetProductLevelString (realInfo.ProductId) : null; | |
Analytics.Identify (user.Email, new Traits { | |
{"firstName", user.FirstName}, | |
{"lastName", user.LastName}, | |
{"email", user.Email}, | |
{"Last Successful License Sync", DateTime.UtcNow}, | |
{"Workstations Used For Activation", usedSlots} | |
}); | |
Analytics.Track (user.Email, "Synced License Successfully", new Properties { | |
{"Resulting License Is Trial", didReturnTrial}, | |
{"Resulting License Level", level}, | |
{"Resulting License Expiration Time", valid_until}, | |
{"License Product", product} | |
}); | |
resp.Write (JsonConvert.SerializeObject (new { | |
code = response.ResponseCode, license = response.License, message = response.Message, | |
messageDetail = response.Description, level, validUntil = valid_until, | |
usedMachines = usedSlots, allowedMachines = availableSlots, | |
nonTrialLevel = couldDowngradeTo, nonTrialValidUntil = didReturnTrial && realInfo != null ? realInfo.ValidUntil.ToString ("u") : null, | |
// if we have trial info, and the trial is expired (valid until <= now), and the trial is not disabled, return true | |
hasHadTrial = trialInfo != null && trialInfo.ValidUntil <= DateTime.UtcNow && trialInfo.Enabled | |
})); | |
} | |
static void TrackError (User user, int errorCode, string message) | |
{ | |
Analytics.Identify (user.Email, new Traits { | |
{"firstName", user.FirstName}, | |
{"lastName", user.LastName}, | |
{"email", user.Email}, | |
{"Last Activation Error Timestamp", DateTime.UtcNow}, | |
{"Last Activation Error", message} | |
}); | |
Analytics.Track (user.Email, "Got Activation Error", new Properties { | |
{"Error Message", message}, | |
{"Error Code", errorCode} | |
}); | |
} | |
static string GetProductLevelString (int productId) | |
{ | |
switch (((ProductId) productId)) { | |
case ProductId.None: | |
return "None"; | |
case ProductId.MonoTouchIndie: | |
case ProductId.MAIndie: | |
case ProductId.XamarinMacIndie: | |
return "Indie"; | |
case ProductId.MonoTouchProfessional: | |
case ProductId.MAProfessional: | |
case ProductId.XamarinMacBusiness: | |
case ProductId.MonoTouchPromotional: | |
case ProductId.MonoTouchAcademic: | |
case ProductId.MAPromotional: | |
case ProductId.MAAcademic: | |
case ProductId.MonoTouchEnterprise: | |
case ProductId.MonoTouchEnterprise5: | |
case ProductId.MAEnterprise: | |
case ProductId.XamarinMacEnterprise: | |
case ProductId.MonoTouchStandard: | |
case ProductId.MAStandard: | |
case ProductId.XamarinMacProfessional: | |
case ProductId.MAEnterprise5: | |
return "Business"; | |
case ProductId.MonoTouchEnterprisePrio: | |
case ProductId.MAEnterprisePrio: | |
case ProductId.MonoTouchPriority: | |
case ProductId.MAPriority: | |
case ProductId.XamarinMacPriority: | |
case ProductId.XamarinMacEnterprisePrio: | |
return "Priority"; | |
case ProductId.MonoTouchTrial: | |
case ProductId.MATrial: | |
return "Trial"; | |
default: | |
return "None"; | |
} | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment